跳轉到

Messenger 整合

nextpdf/symfony 透過 Symfony Messenger 元件提供非同步 PDF 生成能力。GeneratePdfMessage 是純粹的 DTO(Data Transfer Object),GeneratePdfHandler 負責實際的文件生成與後處理。


PHP Compatibility

This example uses PHP 8.5 syntax. If your environment runs PHP 8.1 or 7.4, use NextPDF Backport for a backward-compatible build.

架構概覽

Controller/Service
    └─▶ MessageBusInterface::dispatch(GeneratePdfMessage)
              └─▶ [Transport: Redis/AMQP/Doctrine]
                        └─▶ GeneratePdfHandler::__invoke()
                                  ├─▶ PdfFactory::create()
                                  ├─▶ 生成 PDF bytes
                                  └─▶ 儲存 + 通知

GeneratePdfMessage DTO

<?php

declare(strict_types=1);

namespace NextPDF\Symfony\Messenger;

final readonly class GeneratePdfMessage
{
    public function __construct(
        public readonly string $templateKey,
        /** @var array<string, mixed> */
        public readonly array $parameters = [],
        public readonly ?string $notifyEmail = null,
        public readonly ?string $storageKey = null,
    ) {}
}

派送訊息

<?php

declare(strict_types=1);

namespace App\Controller;

use NextPDF\Symfony\Messenger\GeneratePdfMessage;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Attribute\Route;

final class ReportController
{
    public function __construct(
        private readonly MessageBusInterface $bus,
    ) {}

    #[Route('/reports/{id}/generate', methods: ['POST'])]
    public function generate(int $id): JsonResponse
    {
        $this->bus->dispatch(new GeneratePdfMessage(
            templateKey: 'annual_report',
            parameters: ['reportId' => $id, 'year' => 2026],
            notifyEmail: '[email protected]',
            storageKey: "reports/annual-{$id}.pdf",
        ));

        return new JsonResponse(['status' => 'queued', 'reportId' => $id]);
    }
}

GeneratePdfHandler

Handler 由 Bundle 自動注冊,透過 #[AsMessageHandler] 屬性聲明:

<?php

declare(strict_types=1);

namespace NextPDF\Symfony\Messenger;

use NextPDF\Symfony\Service\PdfFactory;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;

#[AsMessageHandler]
final class GeneratePdfHandler
{
    public function __construct(
        private readonly PdfFactory $factory,
        private readonly PdfTemplateRegistry $templateRegistry,
        private readonly PdfStorageInterface $storage,
        private readonly PdfNotifierInterface $notifier,
    ) {}

    public function __invoke(GeneratePdfMessage $message): void
    {
        // 從 templateKey 取得模板建構邏輯
        $builder = $this->templateRegistry->get($message->templateKey);

        // 生成 PDF
        $document = $this->factory->create();
        $builder->build(document: $document, parameters: $message->parameters);
        $pdfBytes = $document->output();

        // 儲存
        if ($message->storageKey !== null) {
            $this->storage->store(key: $message->storageKey, content: $pdfBytes);
        }

        // 通知
        if ($message->notifyEmail !== null) {
            $this->notifier->notify(email: $message->notifyEmail, storageKey: $message->storageKey);
        }
    }
}

模板 Registry

透過服務標籤(Service Tag)注冊自訂 PDF 模板建構器:

// services.yaml
services:
    App\Pdf\Templates\InvoiceTemplate:
        tags:
            - { name: 'nextpdf.template', key: 'invoice' }

    App\Pdf\Templates\AnnualReportTemplate:
        tags:
            - { name: 'nextpdf.template', key: 'annual_report' }
<?php

declare(strict_types=1);

namespace App\Pdf\Templates;

use NextPDF\Core\Document;
use NextPDF\Symfony\Contract\PdfTemplateInterface;

final class InvoiceTemplate implements PdfTemplateInterface
{
    /** @param array<string, mixed> $parameters */
    public function build(Document $document, array $parameters): void
    {
        $invoice = Invoice::find($parameters['invoiceId']);

        $document->addPage();
        $document->text("Invoice #{$invoice->id}", x: 20, y: 30, fontSize: 20);
        $document->text($invoice->customerName, x: 20, y: 50);
        // ... 更多內容
    }
}

重試與失敗策略

# config/integrations/messenger.yaml
framework:
    messenger:
        transports:
            async:
                dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
                retry_strategy:
                    max_retries: 3
                    delay: 1000      # 初始延遲(毫秒)
                    multiplier: 2    # 指數退避倍數
                    max_delay: 30000 # 最長延遲(毫秒)
            failed:
                dsn: 'doctrine://default?queue_name=failed'

        routing:
            'NextPDF\Symfony\Messenger\GeneratePdfMessage': async

        failure_transport: failed

消費者(Worker)啟動

# 啟動 Messenger Worker(消費 async 傳輸的訊息)
php bin/console messenger:consume async --limit=100 --memory-limit=256M

# 使用 Supervisor 管理 Worker 程序
php bin/console messenger:consume async --time-limit=3600

測試

use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Messenger\Transport\InMemoryTransport;

final class GeneratePdfHandlerTest extends KernelTestCase
{
    public function testHandlerGeneratesPdf(): void
    {
        $kernel = static::bootKernel();

        // 使用 in-memory 傳輸測試
        $this->assertNotNull($kernel->getContainer());

        $bus = static::getContainer()->get('messenger.default_bus');
        $bus->dispatch(new GeneratePdfMessage(
            templateKey: 'invoice',
            parameters: ['invoiceId' => 1],
        ));

        /** @var InMemoryTransport $transport */
        $transport = static::getContainer()->get('messenger.transport.async');
        $this->assertCount(1, $transport->getSent());
    }
}

參見