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

littlerocket/pdf-bundle

littlerocket/pdf-bundle adds PDF generation to PHP applications, wrapping a PDF engine in a simple, configurable service for rendering HTML to PDF and streaming or saving documents. Lightweight setup and sensible defaults for common PDF workflows.

View on GitHub
Deep Wiki
Context7

Getting Started

Know what you're adopting first. littlerocket/pdf-bundle is a maintained-by-nobody fork of a Symfony bundle wrapping PHPPdf — an XML/CSS-driven PDF renderer (you describe the document as XML, PHPPdf lays it out using a CSS-like stylesheet, and emits the PDF via ZendPdf). The fork exists solely to make it install on PHP 7. The upstream library has been unmaintained since ~2015 and the bundle has zero dependents, so treat this as "vendor code you now own," not a dependency you'll get fixes from.

Install and pin hard:

composer require littlerocket/pdf-bundle
# then immediately inspect the constraints you just accepted:
composer show littlerocket/pdf-bundle
composer show phppdf/phppdf   # transitive; check the actual version you got

Check composer.json of both packages for php and symfony/* constraints before going further. If you're on PHP 8.x or Symfony 6.x, stop here — see Gotchas.

Registration (Symfony 3/4 style, which is what this bundle expects):

// config/bundles.php
LittleRocket\PdfBundle\LittleRocketPdfBundle::class => ['all' => true],

Where to look first, in this order:

  1. vendor/littlerocket/pdf-bundle/DependencyInjection/Configuration.php — the authoritative config tree. Don't trust README snippets; this file is the truth for key names.
  2. vendor/littlerocket/pdf-bundle/Resources/config/*.xml — the real service IDs and their visibility. Note whether the facade service is public; if not, you'll need an alias in your own config.
  3. vendor/phppdf/phppdf/lib/PHPPdf/Core/FacadeBuilder.php — the object you actually care about, and the API you'll code against (createDocument(), render(), setStylesheets(), setFonts()).

Minimal config (shape varies by version — verify against Configuration.php):

# config/packages/little_rocket_pdf.yaml
little_rocket_pdf:
    fonts:
        - '%kernel.project_dir%/assets/fonts/DejaVuSans.ttf'
    stylesheets:
        - '%kernel.project_dir%/assets/pdf/base.css'
    cache: '%kernel.cache_dir%/phppdf'

First use case — an invoice/statement PDF in a controller. The natural fit for PHPPdf is structured, template-driven documents: invoices, delivery notes, reports with headers/footers and repeating layouts. It is a bad fit for "render this HTML page to PDF" — that's a wkhtmltopdf/Gotenberg job.

public function invoice(int $id, Facade $facade, Environment $twig): Response
{
    $xml = $twig->render('pdf/invoice.xml.twig', [
        'invoice' => $this->invoiceRepo->find($id),
    ]);

    $path = tempnam(sys_get_temp_dir(), 'pdf_');
    $facade->render($xml, $path);

    return new BinaryFileResponse($path, 200, [
        'Content-Type' => 'application/pdf',
        'Content-Disposition' => 'inline; filename="invoice-'.$id.'.pdf"',
    ]);
}

If Facade isn't autowirable, alias the bundle's service ID to it in your services.yaml — that one line protects you from the bundle's naming choices and makes the rest of your code swappable.

Implementation Patterns

Template the XML with Twig, never concatenate strings. This is the single highest-value pattern. PHPPdf consumes an XML string; Twig gives you escaping, loops, and partials for free.

{# templates/pdf/invoice.xml.twig #}
<?xml version="1.0" encoding="UTF-8"?>
<document>
  <dynamic-page>
    <placeholders>
      <header>
        <area>
          <p class="brand">Acme Ltd — Invoice {{ invoice.number }}</p>
        </area>
      </header>
    </placeholders>

    <h1>Invoice {{ invoice.number }}</h1>
    <p>Issued {{ invoice.issuedAt|date('Y-m-d') }}</p>

    <table>
      {% for line in invoice.lines %}
        <row>
          <cell>{{ line.description }}</cell>
          <cell>{{ line.amount|number_format(2, '.', '') }}</cell>
        </row>
      {% endfor %}
    </table>
  </dynamic-page>
</document>

Keep the <?xml ... ?> prolog inside the template and set Twig autoescape to html — XML escaping and HTML escaping coincide for the characters that matter (& < > " '). Verify your element/attribute vocabulary (table/row/cell vs. whatever your PHPPdf version supports) against vendor/phppdf/phppdf/lib/PHPPdf/Core/Node/ before building templates — the node set is smaller than HTML's and guessing wastes an afternoon.

Keep layout in stylesheets, data in XML. The CSS-like stylesheet is where @page size/margins, typography, and header/footer styling belong. Registering it once via config beats setStylesheets() on every call, and it means a designer can change the look without touching PHP.

Wrap the facade in your own renderer service. One small class, injected everywhere, is the pattern that survives this package's inevitable replacement:

final class PdfRenderer
{
    public function __construct(private Facade $facade, private Environment $twig) {}

    public function render(string $template, array $context): string
    {
        $xml  = $this->twig->render($template, $context);
        $path = tempnam(sys_get_temp_dir(), 'pdf_');
        try {
            $this->facade->render($xml, $path);
            return file_get_contents($path);
        } finally {
            @unlink($path);
        }
    }
}

Later, swapping the body for a Gotenberg HTTP call or dompdf is a one-file change.

Never generate PDFs synchronously on a user's request path. PHPPdf is slow (seconds per document for anything non-trivial) and memory-hungry. The pattern that works: enqueue a message with the document ID, generate in a worker, write to storage (Flysystem/S3), and let the user download the finished artifact. This also gives you a natural place to retry and to cache.

Cache by content hash, not by document ID. Invoices get re-issued; a stale PDF keyed on ID alone is a support ticket. Key on sha1($xml . $stylesheetVersion) and store under var/cache/pdf/. Invalidate the whole namespace when stylesheets or fonts change — bake a version constant into the key and bump it on deploy.

Headers, footers, and page numbers go through placeholders (as above) rather than being re-emitted per page — this is the feature that makes PHPPdf worth considering at all for multi-page invoices. Test with a document that genuinely spans 3+ pages; single-page output hides placeholder bugs.

Testing. Don't attempt pixel comparisons. Assert the byte stream starts with %PDF-, that page count matches expectation (parse with smalot/pdfparser if you need it), and that a cheap text extraction contains the invoice number. For unit tests, fake the PdfRenderer interface at the boundary — testing PHPPdf's layout engine is testing someone else's abandoned code.

Gotchas and Tips

PHP 8 will bite you, and the failure mode is ugly. The fork's "fixed for PHP7" scope means the transitive stack (ZendPdf, Zend\Cache, Zend\Barcode from the Zend Framework 2 era) predates PHP 8. Expect each(), create_function, curly-brace string offsets, dynamic property deprecations turning into TypeErrors, and libxml_disable_entity_loader removal. If composer require succeeds on PHP 8 but a render throws deep inside Zend, that's why — the constraint was never widened, you got lucky/unlucky. Pin PHP in platform config or don't adopt it.

Do not pull this into a new project. With 0 stars and 0 dependents, there is no community to catch regressions. The realistic use case is maintaining an existing Symfony 3/4 + PHP 7 app that already depends on it. For greenfield, use knplabs/knp-snappy-bundle + wkhtmltopdf, mpdf/mpdf (pure PHP, actively maintained, handles HTML/CSS, no binary), or Gotenberg via HTTP. All three are trivially better supported.

Silent XML failures. DOMDocument::loadXML errors surface as generic exceptions. Wrap the render() call, catch \Exception, and log the offending XML to a file (redact PII) plus libxml_get_errors(). A malformed template produces a broken layout, not necessarily an exception — always eyeball the first output of a new template.

Cache directory permissions are a classic prod-only failure. The configured cache path must be writable by the PHP user. var/cache/ is often www-data-owned while your deploy user is different; if you clear the cache during deploy after generating stylesheet caches, be sure the directory gets recreated with the right ownership.

Fonts are the #1 source of "it works locally." The base PDF fonts are Latin-1 only. Any accented character, currency symbol outside ASCII, or CJK text requires a TTF registered via config and enough memory for the metrics. Budget for this up front — the font list is a config change, but the surprise is discovering it after the invoice template is "done."

Memory and timeouts. A dense 50-page document can exceed memory_limit. Set explicit memory_limit and max_execution_time for the CLI worker (not the web process), and chunk very large reports rather than one giant dynamic-page.

Extension points are thin, and that's the risk. If you need a custom node type or output tweak, you're writing a class against PHPPdf internals (PHPPdf\Core\Node\*, formatters) that no one documents. Before doing that, replace the bundle's facade service entirely with your own factory — construct via FacadeBuilder::create() in a service definition so you control stylesheets, fonts, and cache adapter directly instead of fighting the bundle's config tree. This is the cleanest escape hatch and also the first step of the migration away.

Write down the exit plan on day one. Abstract behind PdfRendererInterface, keep all document XML in Twig templates (portable), and keep stylesheets as plain CSS-ish files (mostly portable to mPDF/WeasyPrint). If those three things hold, replacing the backend is a few days of layout tweaking rather than a rewrite — which, for a package with a score of 0.06, is the most important architectural decision you'll make here.

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.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core