Framework Integration
A fluent, drop-in service to generate and merge PDFs from a Laravel application.
Add the URL of your instance to config/services.php:
// config/services.php
'pdf' => [
'api_url' => env('PDF_GEN_API_URL', 'http://localhost:3000/api/gen'),
],# .env
PDF_GEN_API_URL=https://pdf.your-domain.dev/api/gen⚠️ Don't use the demo instance (https://pdf.mathieutu.dev) in production: it is not versioned and can break without notice. Fork the project and deploy your own instance (Docker or Vercel).
This fluent service builds a POST /api/gen call from a Blade view, raw HTML, and/or URLs (web pages, PDFs or images) to merge. It implements Responsable so it can be returned directly from a controller.
<?php
declare(strict_types=1);
namespace App\Services;
use Illuminate\Contracts\Support\Responsable;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use RuntimeException;
class PdfGenerator implements Responsable
{
private readonly string $apiUrl;
private ?string $html = null;
/** @var string[] Pages, PDF or image URLs, merged after the HTML in this order */
private array $urls = [];
private ?string $filename = null;
private string $document;
public function __construct()
{
$this->apiUrl = config('services.pdf.api_url');
}
public function view(string $view, array $data = []): self
{
return $this->html(view($view, $data)->render());
}
public function html(string $html): self
{
$this->html = $html;
return $this;
}
public function url(string $url): self
{
$this->urls[] = $url;
return $this;
}
/** @param iterable<string> $urls */
public function urls(iterable $urls): self
{
foreach ($urls as $url) {
$this->url($url);
}
return $this;
}
public function filename(string $filename): self
{
$this->filename = $filename;
return $this;
}
public function getDocument(): string
{
if (! isset($this->document)) {
$response = Http::asJson()->post($this->apiUrl, array_filter([
'html' => $this->html,
'urls' => $this->urls,
]));
if ($response->failed()) {
throw new RuntimeException("Failed to generate PDF: {$response->body()}");
}
$this->document = $response->body();
}
return $this->document;
}
public function inlineResponse(?string $filename = null): Response
{
return $this->toHttpResponse('inline', $filename);
}
public function downloadResponse(?string $filename = null): Response
{
return $this->toHttpResponse('attachment', $filename);
}
public function save(string $path, ?string $filename = null, ?string $disk = null): string
{
$path = mb_rtrim($path, '/').'/'.($filename ?? $this->filename ?? Str::random(40).'.pdf');
Storage::disk($disk)->put($path, $this->getDocument());
return $path;
}
public function toResponse($request): Response
{
return $this->inlineResponse();
}
private function toHttpResponse(string $disposition, ?string $filename): Response
{
$filename ??= $this->filename ?? 'document.pdf';
return new Response($this->getDocument(), 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => "{$disposition}; filename=\"{$filename}\"",
]);
}
}From a controller (inline response)
use App\Services\PdfGenerator;
class InvoiceController extends Controller
{
public function show(Invoice $invoice, PdfGenerator $pdf)
{
return $pdf->view('invoices.pdf', ['invoice' => $invoice])
->filename("facture-{$invoice->number}.pdf")
->inlineResponse();
}
}Forced download
return $pdf->view('invoices.pdf', ['invoice' => $invoice])
->downloadResponse("invoice-{$invoice->number}.pdf");Merge an HTML cover page with existing PDFs/URLs
return $pdf->view('invoices.cover', ['invoice' => $invoice])
->urls($invoice->attachments->pluck('url'))
->downloadResponse('full-file.pdf');Generate and store outside a controller (job, command...)
$path = app(PdfGenerator::class)
->view('reports.monthly', ['report' => $report])
->save('reports', disk: 's3');| Method | API field | Description |
|---|---|---|
view($view, $data) | html | Renders a Blade view to HTML |
html($html) | html | Raw HTML content |
url($url) / urls($urls) | urls | A page, PDF or image URL, merged in the order it was added |
filename($name) | — | File name used for the HTTP response or save() |
For direct file uploads (PDF/image/HTML as multipart/form-data, 4 MB max), see the multipart form data section of the project's README.

Trainer and pragmatic full-stack lead developer, currently open to short freelance assignments (let's talk!). Passionate about PHP and TypeScript, regular open-source contributor.
When I'm not in front of a screen, you'll usually find me cycling around Lyon, or in the surrounding mountains, where I spend a fair amount of time underground and under waterfalls...
pdf-gen is an open-source side project, born from the need for a simple, self-hostable API to generate and merge PDFs without vendor lock-in or per-document pricing.