paperdoc-dev/paperdoc-lib
A Laravel/PHP library for generating documents programmatically, with an expressive API for composing content and rendering polished, print-ready output. It integrates cleanly into Laravel apps and supports reusable templates and layouts for reports, invoices, and exports.
The repository link is not resolvable, so treat the vendor/paperdoc-dev/paperdoc-lib source as the primary documentation until you confirm otherwise. Budget 20 minutes for a README + src/ scan before writing any application code — a package with a modest star count and a recent release usually ships a thin README and relies on typed method signatures.
Minimal path:
composer require paperdoc-dev/paperdoc-lib
php artisan vendor:publish --provider="Paperdoc\PaperdocServiceProvider"
Publish tags are the usual paperdoc-config, paperdoc-views, paperdoc-assets; run php artisan vendor:publish with no args once to list the actual tags. Then check config/paperdoc.php for the driver/engine setting, default storage disk, and any binary path (headless browser, wkhtmltopdf, etc.). Confirm the package's service provider is auto-discovered via bootstrap/providers.php (Laravel 11+) or config/app.php.
Verify the container binding before you write anything real:
php artisan about
php artisan tinker --execute="dump(array_keys(app()->getBindings()));"
Look for the facade class in src/Facades/ and the contract it resolves. That pair tells you what is safely mockable in tests and what is a concrete implementation you should not type-hint against.
First use case: generate a single document from a Blade-style template and stream it to the browser — no queue, no storage. Get the happy path working end to end on local before touching config. If your first attempt is a queued, multi-tenant, stored-to-S3 build, you will be debugging infrastructure and the package at the same time.
Inject the contract, not the facade. Resolve the interface in constructors for jobs, commands, and services. Keep the facade for controllers and tinker. This is the single biggest lever for testability, and it also protects you if the package swaps its concrete class in a minor release.
Wrap generation in your own thin service. Do not scatter Paperdoc::make(...)->with(...)->render() calls across controllers. Create App\Documents\DocumentBuilder (or similar) that accepts a DTO or array of view data, applies your house style (margins, fonts, header/footer), and returns the raw bytes or a stored path. When the package's API shifts — likely for a package at this maturity level — you change one file.
Template + data separation. Treat templates as versioned assets, not PHP string concatenation. Pass a plain array or DTO into the render call and keep the template free of queries. If the package supports view names, register a namespace (View::addNamespace('docs', resource_path('views/documents'))) so document templates do not collide with web views.
Queue anything user-facing. Generation is CPU- and memory-bound. Dispatch a job, write to a storage disk, hand back a signed temporary URL:
Storage::disk(config('paperdoc.disk'))->put($path, $bytes);
URL::temporarySignedRoute('documents.show', now()->addMinutes(15), ['path' => $path]);
Return the path string from the job, not a model instance holding a stream. Serializing a resource or a temp-file handle into queue payloads is a common and painful mistake.
Testing. If the package ships a fake (Paperdoc::fake() or a swap-in fake implementation), use it and assert on the arguments passed to render. If it does not, bind a small stub of the contract in the container in your test case. Test your wrapper's behavior, not the package's byte output — asserting on PDF bytes is brittle across library versions and font environments.
Integration tips. Register a custom disk in config/filesystems.php rather than reusing local, so retention policies are explicit. Add a paperdoc log channel if the package logs generation failures, and add a failed handler on the job to clean up orphaned temp files. For multi-tenant apps, override the config at runtime via config(['paperdoc.…' => $tenant->setting]) inside the job, after the tenant is initialized — not at boot.
Config caching bites first. php artisan config:cache freezes config/paperdoc.php. Any runtime config([...]) override must happen after the container boots and after the package reads the value — if the package reads the config in its constructor or in a singleton binding, your override silently does nothing. Test with config:cache enabled before shipping.
Binary and font dependencies. Document packages almost always shell out or bundle fonts. Check whether the engine needs a system binary, and pin the version in your Dockerfile. Missing fonts produce silent fallback glyphs (tofu) rather than errors — render a template with non-Latin characters, currency symbols, and emoji as an early smoke test.
Memory and time limits. Raise memory_limit and max_execution_time inside the job, not globally. Large tables with page breaks are the usual culprit. If the package streams to a temp file, use tempnam(sys_get_temp_dir(), 'paperdoc') and clean up in a finally block; leaked temp files will fill the disk on a busy queue worker.
Concurrency. If the engine writes a shared cache or temp directory, parallel workers can collide. Give each job a unique working directory, or run generation on a dedicated queue with --workers=1 if the package has global state.
Extension points to check before forking. Look for: a driver/engine manager (register a custom driver via extend), a template resolver or compiler interface, event hooks around render (e.g. BeforeRender, DocumentGenerated), and a formatter/macro registry. A custom driver is usually the cleanest path for adding an engine the package does not support — it is supported and will survive upgrades.
Version drift. With an unreleased-history repository and a recent release date, pin an exact version in composer.json ("paperdoc-dev/paperdoc-lib": "1.2.*") and read the changelog before every bump. Watch for changes to the config file — published configs are not republished automatically, so new keys will be null until you diff the package's config stub against yours.
Debugging. When output is blank or malformed, dump the intermediate HTML/markup to a file before the engine consumes it — most bugs are in the template layer, not the engine. Set the package's debug/verbose flag if one exists; if it only surfaces a generic exception, catch and wrap it with the document name and tenant ID so logs are actionable.
Where to look when stuck: src/ for the concrete renderer, tests/ for usage examples the README omitted, and the config stub for flags defaulting to values you did not expect. For a package at this score, those three are more reliable than any external documentation.
How can I help you explore Laravel packages today?