gridonic/princexml-php
A lightweight PHP wrapper for PrinceXML that simplifies converting HTML and CSS into high-quality PDFs. It offers a clean API for running Prince, handling input/output files and binary paths, making it easy to generate print-ready documents from PHP or Laravel applications.
Install the wrapper (it is still on Packagist but archived — expect it to just work as a thin shell shim):
composer require gridonic/princexml-php
The package is not a PDF engine. It shells out to the commercial prince binary. Install that first on every machine and CI container that will render PDFs:
prince --version # must resolve on PATH, or pass an absolute path
Check the source before you write code — this is a PSR-0 conversion from 2017, so confirm the class name in src/ (the entry point is the Prince class; the autoload mapping is PSR-0, not PSR-4). Instantiate it with the binary path, then convert a rendered Blade view:
use PrinceXMLPhp\Prince; // confirm exact FQCN in src/ before adopting
$prince = new Prince(config('pdf.prince_bin')); // e.g. '/usr/bin/prince'
$prince->setHTML(true);
$prince->setLog(storage_path('logs/prince.log'));
$prince->convert_string_to_file(
view('invoices.pdf', ['invoice' => $invoice])->render(),
storage_path("app/invoices/{$invoice->id}.pdf"),
config('app.url') // base URL for relative CSS/images
);
return response()->download(storage_path("app/invoices/{$invoice->id}.pdf"));
That Blade → HTML string → PDF file → download loop is the first use case and covers most of what you'll ever do with it.
Bind it in a service provider, don't instantiate ad hoc. There is no Laravel package auto-discovery, facade, or config file here — you own that wiring:
// config/pdf.php
return [
'prince_bin' => env('PRINCE_BIN', '/usr/bin/prince'),
'base_url' => env('APP_URL'),
'log' => storage_path('logs/prince.log'),
];
// AppServiceProvider::register()
$this->app->bind(Prince::class, fn () => tap(new Prince(config('pdf.prince_bin')), function ($p) {
$p->setHTML(true);
$p->setLog(config('pdf.log'));
}));
Wrap it behind your own interface. Because the package is archived with zero dependents, isolate it so you can swap in symfony/process + the prince CLI, or a different engine, without touching controllers:
interface PdfRenderer { public function render(string $html, ?string $baseUrl = null): string; }
class PrinceRenderer implements PdfRenderer {
public function __construct(private Prince $prince) {}
public function render(string $html, ?string $baseUrl = null): string {
return $this->prince->convert_string_to_string($html, $baseUrl ?? config('pdf.base_url'));
}
}
Prefer *_to_string for HTTP responses. Return the bytes directly and skip temp-file cleanup:
$pdf = app(PdfRenderer::class)->render(view('reports.monthly', $data)->render());
return response($pdf, 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'inline; filename="report.pdf"',
]);
Use *_to_file when the PDF is large (avoids holding megabytes in memory) or when you store it for later.
Queue anything non-trivial. Conversion is synchronous and blocks the PHP worker for the full render. A 50-page report with images will hang a request. Dispatch a job, write the file to disk, then notify/download:
GenerateInvoicePdf::dispatch($invoice);
Asset and stylesheet resolution is the usual integration friction. Relative url() and <link href> resolve against the base URL passed to convert_*. Either pass a publicly reachable URL (config('pdf.base_url')) or stage assets on disk and pass a file:// base. If you use the wrapper's addStyleSheet() / .addScript(), they are passed to the CLI as separate arguments — handy for injecting a shared print.css on top of every document.
Design the HTML for PrinceXML, not the browser. Paged media CSS is where the engine earns its money: @page { size: A4; margin: 20mm }, @page :first, running headers via string-set, page-break-before, named pages, footnotes. Put that in a dedicated print stylesheet, not your app CSS.
Test with a skip when the binary is absent.
protected function setUp(): void {
parent::setUp();
if (!is_executable(config('pdf.prince_bin'))) {
$this->markTestSkipped('prince binary not installed');
}
}
app/Support/ as a classmap entry — it's ~a few hundred lines.proc_open/exec must not be in disable_functions, and the web user needs execute permission on the binary. On Windows, quoting differs — test there if you target it.setLog(...) and read the Prince log — it lists every missing resource and CSS parse warning.escapeshellarg protects you, but don't pass user-controlled paths. Never route a raw request parameter into an input/output filename. Generate paths yourself and keep the license file outside the public root.max_execution_time cleanly (it's an external process). Set an explicit job timeout, and cap document size in your app before calling out. Clean up temp files in a finally.addStyleSheet/addScript, log path, and encrypt/key options. If you need streaming, per-page metadata, or job cancellation, you'll be editing the library. Fork and vendor it rather than fighting upstream.spatie/browsershot (headless Chrome) or dompdf are actively maintained, pure-PHP-friendly, and don't require a license. Use this package when Prince's print engine quality (footnotes, cross-references, PDF/X, tagged PDF) is the actual requirement.How can I help you explore Laravel packages today?