rekalogika/print-contracts
rekalogika/print-contracts provides PHP interfaces and contracts for printing workflows, covering print jobs, documents, media, and printer services. It gives libraries and frameworks a shared, implementation-agnostic API for integrating any compatible print backend.
rekalogika/print-contracts is a thin, framework-agnostic set of PHP abstractions (interfaces and value objects) for turning domain objects into PDFs and routing them to printers. It ships no runtime driver — it is the seam you code against, and a separate implementation package (e.g. rekalogika/print-dompdf, rekalogika/print-wkhtmltopdf, or a Gotenberg client) plugs in behind it.
Install both the contracts and a driver in one go:
composer require rekalogika/print-contracts rekalogika/print-dompdf
Where to look first:
src/PrinterInterface.php — the entry point. Expect a single method in the shape of print(object $printable): string (binary PDF) or an equivalent returning a result/stream object.src/PrintableInterface.php (or a Printable trait) — the contract your own models/view models implement to declare what gets rendered.First use case — render a Laravel invoice to a streamed download:
// app/Providers/AppServiceProvider.php
public function register(): void
{
$this->app->singleton(PrinterInterface::class, function () {
return new DompdfPrinter(config('services.print'));
});
}
// app/Models/Invoice.php (implements the package's Printable contract)
class Invoice implements PrintableInterface
{
public function getPrintableContent(): string
{
return view('invoices.pdf', ['invoice' => $this])->render();
}
}
// app/Http/Controllers/InvoiceController.php
public function download(Invoice $invoice, PrinterInterface $printer)
{
$pdf = $printer->print($invoice);
return response($pdf, 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename="invoice-'.$invoice->id.'.pdf"',
]);
}
That's the full loop: implement Printable, bind one PrinterInterface implementation, inject the interface. Everything else (HTML-to-PDF engine, paper size, margins) is the driver's concern.
Code against the interface, never the driver class. The whole point of a contracts package is substitutability. Controllers, jobs, and listeners should type-hint PrinterInterface. Swap DompdfPrinter for a Gotenberg-backed implementation in a config toggle and nothing downstream changes.
Wrap Blade-rendered HTML in a dedicated printable value object. Don't implement PrintableInterface directly on Eloquent models — it welds your domain layer to a rendering concern and to the package. A small DTO per document is cleaner and testable in isolation:
final class InvoicePdf implements PrintableInterface
{
public function __construct(public readonly Invoice $invoice) {}
public function getPrintableContent(): string
{
return view('pdf.invoice', ['invoice' => $this->invoice])->render();
}
}
Queue PDF generation for anything non-trivial. Wkhtmltopdf and headless Chrome are slow. Follow the synchronous interface for small docs, but for bulk (100 statements, monthly run) dispatch a job that injects the printer, calls print(), writes to Storage, and emails a link. The contracts package doesn't care whether you're in a request or a worker.
Decorator pattern for cross-cutting concerns. Because everything is an interface, you can wrap the real printer with logging, timing, caching, or an audit trail without touching the driver:
final class LoggingPrinter implements PrinterInterface
{
public function __construct(
private PrinterInterface $inner,
private LoggerInterface $log,
) {}
public function print(object $printable): string
{
$start = microtime(true);
$pdf = $this->inner->print($printable);
$this->log->info('PDF generated', ['ms' => (int) ((microtime(true) - $start) * 1000)]);
return $pdf;
}
}
Bind the real one as the concrete class, then bind PrinterInterface to the decorator.
Multiple printers via naming. If you have both a fast in-process engine (Dompdf, for previews) and a high-fidelity remote one (Prince, a hosted service), register two container bindings keyed by context — print.fast and print.high-fidelity — and resolve the right one in a factory. Don't try to make one driver do both.
Route through a service layer for testability. A DocumentService that depends on PrinterInterface gives you one place to stub in tests and one place to add authorization, retention policy, and watermarks.
Integration tip — response types. The contract likely returns a PHP binary string. For large PDFs, prefer streaming to a temporary file (Storage::disk('local')) and response()->streamDownload() rather than holding tens of MB in memory.
It's contracts only. If composer require rekalogika/print-contracts alone is installed, container resolution of PrinterInterface will fail. You must add an implementation package and bind it. The zero stars / zero dependents is a signal: there is no ready Laravel service provider, so expect to write 10–15 lines of AppServiceProvider wiring yourself.
Symfony-shaped, not Laravel-shaped. Rekalogika's ecosystem targets Symfony bundles with autowired services. A Laravel app has no auto-wiring for these — you register singleton bindings manually. Don't expect php artisan vendor:publish for config.
Interface churn. Contracts packages rev freely in 0.x. Pin the constraints ("rekalogika/print-contracts": "~0.1.0") and read the UPGRADE.md on every minor bump before composer update.
The printable's render method name matters. If your implementation returns a file path versus HTML versus markup object, the driver's contract determines whether it's render(), getContent(), or __toString(). Check the interface signature against the driver version — mismatches between contracts version and driver version are the number-one reason a "working" example throws a TypeError.
Debugging blank PDFs. Almost always whitespace from a stray @php ... @endphp emitting a BOM, or a view()->render() returning empty because the DTO passed the wrong data. Log strlen($pdf) before the response; a valid empty PDF is still ~1KB, whereas an empty string is a red flag.
Paper size and DPI are driver properties, not contract properties. The contracts package may carry paper-size value objects, but margins, DPI, and header/footer templates are implementation details of whatever engine you chose. Configuring them on the abstraction wastes time — drop into the concrete driver's config array.
Timeouts in workers. Gotenberg/Chrome-backed drivers hang for 30–60s under load. Set an explicit timeout in the HTTP client your driver uses; the contracts layer won't do it for you. A queue worker's default 60s --timeout is often too short for bulk PDF runs.
Testing seam. Because PrinterInterface is one method, a FakePrinter returning a canned 1-page PDF is trivial. Assert on the printable (which view was rendered, with what data) rather than the binary bytes — binary PDF assertions are brittle.
Extension points. If you need a new rendering capability (SVG inlining, signed PDFs, custom fonts), don't fork the contracts package — extend at the driver level or implement a decorator around PrinterInterface. The contracts are intentionally minimal; fighting that is a symptom you've picked the wrong layer to solve the problem.
How can I help you explore Laravel packages today?