Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Pdf Bundle Laravel Package

psliwa/pdf-bundle

Symfony bundle for generating PDFs from HTML with wkhtmltopdf. It wraps the psliwa/php-pdf library, integrates with Twig templates, and exposes configurable PDF options, making it straightforward to render invoices, reports, and other documents in Symfony applications.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and register. The bundle is a thin Symfony wrapper around the psliwa/php-pdf library (namespace PHPPdf). Require it, then register Psliwa\PdfBundle\PsliwaPdfBundle in AppKernel (or config/bundles.php on Symfony 4+).

composer require psliwa/pdf-bundle

2. Configure it. The bundle exposes a psliwa_pdf config block (cache and temp directories matter a lot on this package). Always confirm the keys for your pinned version before copying an example:

bin/console config:dump-reference psliwa_pdf
# config/packages/psliwa_pdf.yaml
psliwa_pdf:
    use_cache: true                       # false in dev
    cache_dir: '%kernel.cache_dir%/pdf'
    temp_dir:  '%kernel.cache_dir%/pdf'

3. Where to look first.

  • The bundle's README.md — shows the one-liner controller usage.
  • The library docs (psliwa/php-pdf), not the bundle, for the XML node reference (<dynamic-page>, <page>, <table>, <barcode>, <stylesheet>…). That's where 90% of your day-to-day answers live.
  • bin/console debug:container psliwa — the facade is registered as a service (typically psliwa.pdf) and is what you inject.
  • vendor/psliwa/pdf-bundle/DependencyInjection/Configuration.php — the real list of options when the README is stale.

4. First use case: a one-page invoice. Build the XML, hand it to the facade, save to a temp file and return a normal Symfony response:

use PHPPdf\Core\Facade;
use Symfony\Component\HttpFoundation\BinaryFileResponse;

public function invoicePdf(Facade $facade): BinaryFileResponse
{
    $xml = <<<XML
    <document>
        <dynamic-page>
            <h1>Invoice #123</h1>
            <p>Thanks for your business.</p>
        </dynamic-page>
    </document>
    XML;

    $document = $facade->createPdf($xml);

    $path = tempnam(sys_get_temp_dir(), 'pdf_') . '.pdf';
    $facade->save($document, $path);

    return $this->file($path, 'invoice.pdf')->deleteFileAfterSend(true);
}

If Facade isn't autowirable in your setup (common on this bundle), fetch it explicitly: $facade = $this->get('psliwa.pdf'); / $container->get('psliwa.pdf'), or add an alias in services.yaml.

Once that renders, the natural next step is moving the XML out of the controller into a Twig template — that's where the package becomes maintainable.

Implementation Patterns

Template-driven XML (the pattern you'll actually use). Keep the document XML in templates/pdf/*.xml.twig. Twig's default autoescaping produces XML-safe output, so user data with &, <, quotes is handled for you.

{# templates/pdf/invoice.xml.twig #}
<document>
    <dynamic-page>
        <stylesheet>
            <h1><attribute name="font-size" value="18" /></h1>
        </stylesheet>
        <h1>Invoice {{ invoice.number }}</h1>
        <table>
            {% for line in invoice.lines %}
                <tr>
                    <td>{{ line.label }}</td>
                    <td>{{ line.total|number_format(2, ',', ' ') }}</td>
                </tr>
            {% endfor %}
        </table>
        <footer>{{ company.name }} — page %page%</footer>
    </dynamic-page>
</document>
$xml = $this->twig->render('pdf/invoice.xml.twig', ['invoice' => $invoice]);
$document = $facade->createPdf($xml);

Wrap it behind your own renderer service. With 118 stars and no dependents, isolate the dependency. One small interface buys you the option to swap to Dompdf/mPDF/wkhtmltopdf later without touching controllers:

interface PdfRendererInterface {
    /** @return string absolute path to a PDF file */
    public function render(string $template, array $context): string;
}

final class PhpPdfRenderer implements PdfRendererInterface
{
    public function __construct(
        private Facade $facade,
        private Environment $twig,
        private string $tmpDir,
    ) {}

    public function render(string $template, array $context): string
    {
        $document = $this->facade->createPdf($this->twig->render($template, $context));
        $path = tempnam($this->tmpDir, 'pdf_') . '.pdf';
        $this->facade->save($document, $path);
        return $path;
    }
}

Controllers then only deal with the Response, and tests can substitute a fake renderer.

Controller → BinaryFileResponse. Prefer save() + BinaryFileResponse over the facade's direct-output helper. You get proper Symfony headers, Content-Disposition, deleteFileAfterSend(), and no risk of a stray whitespace/notice corrupting the stream.

Cache expensive documents. Deterministic invoices/reports don't need regenerating. Hash the template name + context, store the PDF in var/cache/pdf/ (or S3), and serve the file if the hash matches. Bypass cache in dev.

Async generation for heavy documents. If a document has many pages or images, push it to a Messenger/Symfony worker job and store the result; the controller returns a "not ready" or a download link. This avoids FPM timeouts and memory_limit surprises on large tables.

Reusable partials. Use Twig {% include %} for letterheads, address blocks, and totals, and use the library's <header>/<footer> nodes inside <dynamic-page> for repeated content. Keeping these in one place makes restyling a single-file change.

Attach to mail. Once you have a path, Symfony\Component\Mime\Email::attachFromPath($path, 'invoice.pdf'). That's the second-most-common use case after "download".

Testing. Don't byte-compare PDFs. Assert:

  • the file starts with %PDF- and is non-trivial in size,
  • (optionally) pdftotext output contains the invoice number,
  • and unit-test the rendered XML string from Twig — that's where your logic bugs are.

Gotchas and Tips

It's old — plan for version friction. The bundle predates Symfony 4/5/6 conveniences. Expect to hand-register the bundle, add service aliases, and possibly --ignore-platform-reqs or a fork on newer PHP. Pin both psliwa/pdf-bundle and psliwa/php-pdf to exact versions; their releases move independently. Verify package name (psliwa/php-pdf) vs. namespace (PHPPdf) when reading docs — they differ.

XML escaping is the #1 bug source. Anything injected into the XML must be escaped. Twig's autoescape does this correctly; hand-built strings need htmlspecialchars($s, ENT_XML1, 'UTF-8'). A company named Müller & Söhne in a hand-concatenated <p> will throw a parse error, and the exception message rarely points at the offending character.

Well-formed XML only. No stray unescaped &, no HTML entities like &nbsp;, no BOM at the top of templates. Control characters from user input also break the parser — strip them before rendering.

<dynamic-page> vs <page>. Automatic pagination and page breaking only work inside <dynamic-page>. A fixed <page> silently clips overflow (or throws) once content exceeds it — a classic "why is my table missing rows?" bug.

Fonts and Unicode. Custom TTF fonts must exist on the filesystem and be readable by the PHP process; paths are resolved by the library, not by Symfony. The default fonts have narrow glyph coverage, so Polish/CJK/emoji characters typically render as ? or boxes. Ship a font with the glyph range you need in your project and reference it by absolute path.

Image paths and size. <img src="..."> resolves relative to the process CWD, not %kernel.project_dir%. Always use absolute paths built from a container parameter. Large images are interpreted at ~72 DPI, which destroys layout — pre-scale them (GD/Imagick) before embedding, and never accept user-controlled paths (LFI risk).

Cache directories must be writable by the web/CLI user, and stale cache survives template edits. Clear %kernel.cache_dir%/pdf as part of deploy, and set use_cache: false in dev while iterating on styles.

Don't echo. The show()-style helpers write straight to output; any prior echo, PHP notice, or whitespace before <?php produces a corrupt PDF that opens as blank in some readers. save() to a temp file plus BinaryFileResponse avoids the whole class of problems.

Debugging strategy. Parsing/style errors surface as generic exceptions. Wrap the createPdf() call, dump the exact XML to var/log/pdf/xxx.xml on failure, and reproduce it against the XML file directly. Most "it randomly fails" reports reduce to one bad character or a missing attribute in the generated XML.

Autowiring may not work. There is often no class alias for the facade service. Use the service id, or add:

services:
    PHPPdf\Core\Facade: '@psliwa.pdf'

Extension points are thin. The bundle's real extension surface is the facade and its collaborators — inspect debug:container psliwa before assuming tag-based registration for custom tags exists. In practice, the safest extensibility is (a) your own PdfRendererInterface wrapper and (b) pre-processing input into the XML you control, rather than subclassing library internals whose API isn't documented.

Treat it as a leaf dependency. Low star count and zero dependents means the bus factor is you. The wrapper interface above, plus a smoke test in CI that renders one document a day, is cheap insurance against a broken upgrade.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
mydnic/volet-feature-board-filament-plugin
wmde/email-address
refinery29/test-util
dees040/laravel-api-responses
teamzac/geocoder
ideatica/config-writer
keaton-php/pipe-dream
jacksonwebservices/laravel-user-suite
maksimru/conditional-laravel-debugbar
gismatul/nova-support-chat
gismatul/laravel-support-chat
networkrailbusinesssystems/oracle-api
alphabalex/messaging-made-easy
rawphp/warp
shootkiran/merobug
narakode/fineauth
numencode/wn-syncops-plugin
ibnnajjaar/mib-global-pay
refinery29/php-cs-fixer-config
parsnick/steak