← Back to home

Framework Integration

Using pdf-gen from Laravel

A fluent, drop-in service to generate and merge PDFs from a Laravel application.

1. Configuration

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).

2. The service

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}\"",
        ]);
    }
}

3. Usage

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');

Parameter reference

MethodAPI fieldDescription
view($view, $data)htmlRenders a Blade view to HTML
html($html)htmlRaw HTML content
url($url) / urls($urls)urlsA 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.

Mathieu TUDISCO

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.

View contributors