Symfony developer guide
At a glance
Section titled “At a glance”The Symfony package puts services first. Inject PdfFactory, call create() for each Portable Document Format (PDF) document, and use Messenger builders for asynchronous generation. You can keep the factory as a container service because each call returns a fresh document.
Use this guide when you design controllers, services, Messenger handlers, or bundle-level extension points for nextpdf/symfony.
Architecture boundary
Section titled “Architecture boundary”| Layer | Owned by | Responsibility | Do not put here |
|---|---|---|---|
| Controller | Application | Authorize the request, collect input, and return PdfResponse. | PDF layout shared across use cases. |
| Application service | Application | Load domain data and select a builder. | Symfony container compiler logic. |
| Builder service | Application | Implement PdfBuilderInterface for synchronous or queued document construction. | Request objects, entity managers, or non-serializable context. |
| Symfony bundle | nextpdf/symfony | Register services, the config tree, optional extension pass, response helpers, and Messenger data transfer objects (DTOs). | Tenant-specific storage policy. |
| Core engine | nextpdf/nextpdf | Create and serialize the document. | Symfony response or Messenger behavior. |
Runtime lifecycle
Section titled “Runtime lifecycle”| Stage | Behavior | Developer action |
|---|---|---|
| Bundle boot | NextPdfBundle::build() registers optional extension detection. | Let Symfony discover the bundle or register it in bundles.php. |
| Config load | NextPdfExtension::load() processes nextpdf: config and loads service definitions. | Keep configuration explicit and environment-aware. |
| Factory use | PdfFactory::create() returns a fresh, configured document. | Do not store documents in services. |
| Controller output | PdfResponse turns a completed document into a response. | Use the helper instead of assembling headers manually. |
| Messenger dispatch | GeneratePdfMessage carries builder class, output path, and serializable context. | Keep context minimal and scalar-friendly. |
| Message handling | GeneratePdfHandler resolves the builder from a service locator and saves the document. | Make builders deterministic and idempotent. |
Recommended application structure
Section titled “Recommended application structure”| Path | Purpose |
|---|---|
src/Pdf/Builder/* | Services that implement PdfBuilderInterface. |
src/Pdf/Data/* | Small DTOs or arrays used as builder context. |
src/Pdf/Storage/* | Storage-root selection and output filename policy. |
src/Controller/* | Synchronous response entry points. |
tests/Pdf/* | Builder, response, Messenger, and config tests. |
Prefer builder services over static helper functions. They are easy to tag, decorate, test, and use from Messenger.
<?php
namespace App\Pdf\Builder;
use NextPDF\Core\Document;use NextPDF\Symfony\Message\PdfBuilderInterface;
final readonly class InvoicePdfBuilder implements PdfBuilderInterface{ public function build(Document $document, array $context): Document { $document->setTitle((string) $context['title']) ->addPage() ->writeHtml((string) $context['html']);
return $document; }}Synchronous response pattern
Section titled “Synchronous response pattern”<?php
namespace App\Controller;
use App\Pdf\Builder\InvoicePdfBuilder;use NextPDF\Symfony\Http\PdfResponse;use NextPDF\Symfony\Service\PdfFactory;
final readonly class InvoiceController{ public function __invoke( PdfFactory $factory, InvoicePdfBuilder $builder, ) { $document = $builder->build($factory->create(), [ 'title' => 'Invoice 1234', 'html' => '<h1>Invoice 1234</h1>', ]);
return PdfResponse::download($document, 'invoice-1234.pdf'); }}Keep controller context small. When a builder needs many domain objects, move orchestration into an application service and pass a DTO or normalized array to the builder.
Messenger pattern
Section titled “Messenger pattern”GeneratePdfMessage validates the builder class and output path before dispatch. The handler validates the path again when it runs.
<?php
use App\Pdf\Builder\InvoicePdfBuilder;use NextPDF\Symfony\Message\GeneratePdfMessage;
$bus->dispatch(new GeneratePdfMessage( builderClass: InvoicePdfBuilder::class, outputPath: $projectDir . '/var/pdfs/invoice-1234.pdf', builderContext: [ 'title' => 'Invoice 1234', 'html' => '<h1>Invoice 1234</h1>', ],));Do not put Doctrine entities, open streams, closures, request objects, or service objects in builderContext.
Extension points
Section titled “Extension points”| Extension point | Use it for | Constraint |
|---|---|---|
PdfFactory service decoration | Applying application defaults before documents reach controllers. | Must preserve fresh-document semantics. |
PdfBuilderInterface | Defining queued or reusable document builders. | Must return a Document. |
OptionalExtensionPass | Enabling optional Artisan or Premium features at compile time. | Availability is container compile state, not request state. |
| Symfony config tree | Defaults, PDF/A, renderer settings, signature, time-stamping authority (TSA), and Messenger. | Invalid config should fail during container build. |
GeneratePdfHandler service wiring | Restricting which builders are reachable from queued messages. | Service locator should expose only approved builder services. |
Development workflow
Section titled “Development workflow”- Add a builder service with deterministic input.
- Use
PdfFactory::create()in a controller or service. - Add a response test for the filename, content type, and headers.
- Register the builder with Messenger when the same document must be generated asynchronously.
- Add invalid-message tests for the class name, output path, and context shape.
- Add a container compilation test with minimal and production configuration.
- Measure render time and memory under the same PHP settings as production.
Failure handling
Section titled “Failure handling”| Failure | Where it should be handled | Recommended response |
|---|---|---|
| Invalid config | Container compilation. | Fail deployment before traffic reaches the app. |
| Missing builder service | Messenger handler tests and service tags. | Fail the message and alert the owning team. |
| Unsafe output path | Message constructor and storage policy. | Reject it before dispatch; keep handler validation as defense in depth. |
| Optional extension unavailable | Compiler pass and factory behavior. | Disable optional feature or make installation explicit. |
| Service conversion or render failure | Builder boundary. | Fail closed unless the use case has a documented fallback. |
Safe defaults
Section titled “Safe defaults”| Concern | Default | When to override |
|---|---|---|
| Factory lifetime | Container service. | Keep this; the factory is safe because it creates fresh documents. |
| Document lifetime | One unit of work. | Never share across requests or messages. |
| Output path validation | Message constructor and handler. | Add tenant or storage-root constraints in application code. |
| Response filename | document.pdf. | Override with sanitized business identifiers. |
| Messenger transport | async. | Use a dedicated transport when PDF work is heavy. |
Testing checklist
Section titled “Testing checklist”- Container tests compile the bundle with minimal and production configuration.
- Response tests assert security headers and filename handling.
- Messenger tests assert invalid paths and invalid builder class names fail before dispatch.
- Handler tests use a real builder service and a temporary output directory.
- Builder tests render a representative document and save it under production-like filesystem permissions.
- Optional extension tests cover Artisan unavailable, Premium unavailable, and configured PDF/A profile behavior.