radweb/pdfcrowd-php
A PHP wrapper for the Pdfcrowd API that converts HTML, URLs, and files into PDFs. Integrates with Laravel and plain PHP apps, exposing Pdfcrowd conversion options through a simple client for generating downloadable or stored PDF documents.
The package is a thin Laravel-flavoured wrapper around PDFCrowd's PHP client: a service provider, a config file, and (usually) a facade that proxies to a Pdfcrowd\HtmlToPdfClient-style object. It is archived, so treat it as a vendored dependency you own rather than something you can file issues against.
Pull it in (expect it to resolve to a pinned commit since there are no new releases):
composer require radweb/pdfcrowd-php
Publish the config and wire credentials:
php artisan vendor:publish --provider="Radweb\Pdfcrowd\PdfcrowdServiceProvider"
Then set your PDFCrowd API credentials — the package reads them from config/pdfcrowd.php, which in turn should read from config/services.php or env():
// config/pdfcrowd.php
return [
'username' => env('PDFCROWD_USERNAME'),
'api_key' => env('PDFCROWD_API_KEY'),
];
Look here first, in this order:
config/pdfcrowd.php — the only knob most teams touch.src/PdfcrowdServiceProvider.php — see what's bound into the container (usually a singleton client factory).src/Facade/Pdfcrowd.php (or similar) — this tells you the exact call surface. Because the wrapper is thin, the real API surface is PDFCrowd's own client docs at pdfcrowd.com/doc/php/.First use case — Blade view to PDF. Render a view to a string, hand it to the client, stream it:
use Radweb\Pdfcrowd\Facades\Pdfcrowd;
Route::get('/invoices/{invoice}/pdf', function (Invoice $invoice) {
$html = view('invoices.pdf', compact('invoice'))->render();
$pdf = Pdfcrowd::convertHtml($html);
return response($pdf, 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'inline; filename="invoice-'.$invoice->id.'.pdf"',
]);
});
If that returns a PDF, you've validated the whole stack (credentials → client → HTML parse → binary response).
Inject the client, don't reach for the facade in domain code. Facades are convenient in controllers, but anything reused (jobs, mailables, services) should take the client — or a small PdfRenderer interface you own — via constructor injection. That keeps you one refactor away from swapping in the official pdfcrowd/pdfcrowd-php SDK when the archived wrapper inevitably rots.
Centralise a PdfRenderer service. The wrapper's fluent setters (setAuthor, setTitle, setPageSize, setMargin*, setHeaderHtml, setFooterHtml, useHttpAuthentication) mutate the client instance, so a shared singleton can leak state between calls. Build a small factory each time:
final class PdfRenderer
{
public function render(string $html, array $opts = []): string
{
$client = app(PdfcrowdClientFactory::class)->make(); // fresh instance
$client->setAuthor(config('app.name'));
$client->setPageSize($opts['page_size'] ?? 'A4');
$client->setFooterHtml('<span>Generated '.now()->toDateString().'</span>');
return $client->convertHtml($html);
}
}
Two rendering flavours, pick deliberately.
convertHtml($html) — full control, works offline for styling, but relative asset URLs break. Either view()->render() with asset URLs made absolute, or use <base href="...">.convertUrl($url, $options) — lets PDFCrowd fetch the page (good for authenticated apps you expose internally, and for SPAs that need JS execution). Pass auth via setHttpAuth() / custom cookies rather than embedding tokens in the URL.Large / slow conversions belong in a queue. HTML-to-PDF is a network round trip plus PDFCrowd-side rendering; a complex report easily takes 5–30s. Job pattern:
public function handle(PdfRenderer $renderer): void
{
$html = view('reports.pdf', ['report' => $this->report])->render();
$pdf = $renderer->render($html);
Storage::disk('s3')->put($this->report->pdfPath(), $pdf);
$this->report->update(['rendered_at' => now()]);
}
Then the controller simply redirects or serves the pre-rendered file. This is the single biggest quality-of-life decision — synchronous PDFCrowd calls are what make users think the app is broken.
Storage vs streaming. Small invoices: return response($pdf)->header(...). Anything reused (statements, receipts): write to disk and serve a signed URL. PDFs are stable artifacts; regenerating on every request burns PDFCrowd credits.
Testing: fake the renderer, not the network. Because the client is a thin singleton, you can bind a stub into the container in setUp():
$this->app->bind(PdfRenderer::class, fn () => new class extends PdfRenderer {
public function render(string $html, array $opts = []): string { return '%PDF-1.4 fake'; }
});
Assert on the generated HTML in feature tests (snapshot the Blade output) and keep one integration test that hits PDFCrowd in a dedicated, credential-gated test group.
Error mapping. Wrap calls and translate the client's PdfcrowdException (wrong credentials, quota exhausted, unsupported input) into domain exceptions so your HTTP layer can return sensible statuses instead of a 500.
pdfcrowd/pdfcrowd-php client. The wrapper's value is the Laravel config/facade sugar, which is ~50 lines you can reimplement.env() calls in config/pdfcrowd.php will silently return null after php artisan config:cache unless the file uses env() only at that top level (which it does if written correctly). If PDFs suddenly fail after a deploy, php artisan config:clear first, then check php artisan tinker → config('pdfcrowd.api_key').setPageSize() in one request, it may persist into the next within a long-running worker (Octane, queue worker). Always make() a fresh client, or reset setters explicitly./css/app.css or storage/... won't resolve in convertHtml. Use asset() / Storage::url() with a fully-qualified host, or inject <base href="{{ config('app.url') }}"> into the layout. This is the #1 cause of "the PDF looks unstyled in production but fine locally".convert* options) and on your queue worker's $timeout.catch (\Throwable $e) you'll render a "PDF" that's actually an error page. Log the exception class and message; do not return $e->getMessage() as PDF content.PdfRenderer interface, (b) override the config path, (c) subclass the facade's underlying client if you need per-tenant credentials (multi-tenant setups need a factory keyed by tenant, not a global singleton). Anything deeper means forking.pdfcrowd php (the upstream SDK) instead; the method names and error types are the same, and that's where all the real documentation lives.How can I help you explore Laravel packages today?