Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Gotenberg Php Laravel Package

gotenberg/gotenberg-php

A PHP client for the Gotenberg API, letting you convert HTML, Markdown, Word, Excel, and URLs into PDFs using Chromium and LibreOffice, with a simple, framework-agnostic interface.

View on GitHub
Deep Wiki
Context7

Getting Started

Two moving parts: the Gotenberg container and the PHP client. You need both before the first PDF exists.

# 1. The conversion engine (run this locally, in CI, and in prod)
docker run --rm -p 3000:3000 gotenberg/gotenberg:8

# 2. The client + a PSR-18 adapter (Guzzle is the path of least resistance in Laravel)
composer require gotenberg/gotenberg-php php-http/guzzle7-adapter

The package is not Laravel-aware — no facade, no service provider, no config file. Wire it yourself in config/services.php:

'gotenberg' => [
    'url' => env('GOTENBERG_URL', 'http://localhost:3000'),
    'timeout' => env('GOTENBERG_TIMEOUT', 120),
],

First use case — an invoice PDF from a Blade view. This is what 90% of Laravel adoption looks like:

use Gotenberg\Gotenberg;
use Gotenberg\Stream;

$html = view('invoices.pdf', ['invoice' => $invoice])->render();

$request = Gotenberg::chromium(config('services.gotenberg.url'))
    ->pdf()
    ->assets(
        Stream::string('style.css', file_get_contents(public_path('css/invoice.css')))
    )
    ->html(Stream::string('index.html', $html))
    ->outputFilename("invoice-{$invoice->number}");

$filename = Gotenberg::save($request, storage_path('app/invoices'));

Where to look first, in order:

  1. The endpoint methods on the three modules — Gotenberg::chromium(), Gotenberg::libreOffice(), Gotenberg::pdfEngines() — these are your entire API surface.
  2. The Stream factory (Stream::path, Stream::string, new Stream(...)) and DownloadFrom, which determine how files reach Gotenberg.
  3. The Gotenberg docs at gotenberg.dev, not the PHP README, for the actual conversion options. The PHP client is a thin, near-mechanical mapping of Gotenberg's form fields onto method calls; knowing the server's capabilities is the real work.

Implementation Patterns

Wrap it in a single service class

The client returns PSR-7 requests and expects a PSR-18 client. That is awkward at a controller's call site. Collapse it into one injectable class, and let the rest of the application never see Gotenberg:: again:

namespace App\Services;

use Gotenberg\Gotenberg;
use Gotenberg\Stream;
use Psr\Http\Client\ClientInterface;

class PdfConverter
{
    public function __construct(private ClientInterface $http) {}

    public function fromHtml(string $html, array $assets = []): string
    {
        $request = Gotenberg::chromium(config('services.gotenberg.url'))
            ->pdf()
            ->assets(...array_map(
                fn ($name, $contents) => Stream::string($name, $contents),
                array_keys($assets),
                $assets
            ))
            ->html(Stream::string('index.html', $html))
            ->correlationId((string) str()->uuid());

        return Gotenberg::save($request, storage_path('app/pdf'), $this->http);
    }
}

Binding ClientInterface in a service provider (e.g. $this->app->bind(ClientInterface::class, fn () => new \Http\Adapter\Guzzle7\Client(...))) is worth the ten lines: it makes the converter testable, lets you tune timeouts globally, and avoids relying on php-http/discovery picking a client at runtime.

Render Blade → HTML → PDF

Never build HTML strings by hand. Blade gives you escaping, components, and layout inheritance for free, and view()->render() returns the exact string Stream::string('index.html', ...) wants. Note that assets() uses the filename as the URL path — your <link href="style.css"> must match the stream name, which means inlining or overriding asset URLs rather than pointing at /css/app.css.

Office documents via LibreOffice

For anything a user uploads (.docx, .xlsx, .pptx), use the libreOffice() module and pass the uploaded file straight through:

$request = Gotenberg::libreOffice($apiUrl)
    ->convert(Stream::path($uploadedFile->getRealPath()))
    ->password($uploadedFile->getClientOriginalName() === 'secret.docx' ? $pw : null)
    ->exportBookmarks()
    ->pdfa('PDF/A-2b');

Stream::path() requires a real filesystem path — UploadedFile::getRealPath() gives you the PHP temp path, which is fine. If you're on a cloud disk, pull the file down first or use Stream::string() with the fetched bytes.

Let Gotenberg fetch the files

downloadFrom() is underused and saves a round trip through your app's memory. When the source document already lives on a URL your backend can reach, skip the upload entirely:

Gotenberg::libreOffice($apiUrl)
    ->downloadFrom([new DownloadFrom($signedUrl, ['Authorization' => "Bearer {$token}"])])
    ->convert();

Note the signature change in v2.7.1: all Stream arguments are now optional on convert() precisely because downloadFrom() can supply the files.

Queue the heavy stuff

Chromium and LibreOffice are slow and single-request-hungry: a 200-page docx easily eats 30–60 seconds. Run conversions in a queued job with an explicit $timeout and $tries, then notify or store:

class GenerateStatementPdf implements ShouldQueue
{
    public int $timeout = 180;
    public int $tries = 2;

    public function handle(PdfConverter $converter): void
    {
        $converter->fromHtml(view('statements.pdf', [...])->render());
    }
}

Synchronous conversion belongs only on small, user-blocking documents (an invoice download button), and even then give the HTTP client a generous timeout.

Return the response directly when the user wants the file

Gotenberg::send() returns a PSR-7 ResponseInterface whose body is the binary PDF. Stream it out of a controller:

public function download(Invoice $invoice): Response
{
    $response = Gotenberg::send(
        Gotenberg::chromium($apiUrl)->pdf()
            ->html(Stream::string('index.html', view('invoices.pdf', compact('invoice'))->render()))
    );

    return response($response->getBody()->getContents(), 200, [
        'Content-Type' => 'application/pdf',
    ]);
}

This avoids writing a temp file you then have to serve and clean up.

PDF engine plumbing

pdfEngines() is where multi-file workflows live: merge() a batch of invoices into one statement, or split() a PDF into a zip. These are genuinely useful in accounting and document-packaging features, and they're one-liners:

Gotenberg::pdfEngines($apiUrl)
    ->merge(Stream::path($a), Stream::path($b), Stream::path($c))
    ->pdfa('PDF/A-3b');

Gotchas and Tips

You must install a PSR-18 adapter. Without php-http/guzzle7-adapter (or equivalent), Gotenberg::send() and Gotenberg::save() rely on php-http/discovery and will blow up at runtime with an unhelpful "no client found" error. Explicitly passing the client (Gotenberg::send($request, $client)) removes the mystery entirely and is the safer habit in production.

Gotenberg::save() writes to a directory, not a Laravel disk. It returns only the filename. On local storage this is fine; on S3 you must use send() and then Storage::put($key, $response->getBody()->getContents()). Remember to rewind() a stream before reading it twice — PSR-7 bodies are not rewindable by default.

Default filename collisions. Gotenberg generates UUID filenames unless you call outputFilename(). Set it deliberately and let Gotenberg append the extension — pass invoice-123, not invoice-123.pdf, unless you want invoice-123.pdf.pdf. If two jobs generate the same fixed name concurrently, they clobber each other; include an ID or timestamp.

The Gotenberg-Trace header must agree with the server. correlationId('debug') sets a header that Gotenberg only echoes back if its --api-correlation-id-header property matches. If your getCorrelationId() returns null on errors, that mismatch is almost always why. This matters more than it sounds: without correlation IDs, diagnosing a failed conversion from Gotenberg's container logs is guesswork.

Catch GotenbergApiErrored, not a generic exception. Non-2xx responses throw, and the exception carries both getResponse() (so you can inspect Gotenberg's message, which is often a Chromium console log) and getCorrelationId(). Log both — the body explains the failure, the trace locates it in the server logs.

Version pairing is rigid. Client v2.x targets Gotenberg 8.x; v1.x targets 7.x. Pointing a v2 client at a v7 container produces 404s on routes that moved (the Chromium PDF/screenshot split into ->pdf() / ->screenshot() is the classic breakage). Pin the Docker tag in your compose file and upgrade the client in the same PR.

Asset URLs in Chromium are resolved server-side. Gotenberg fetches URLs from inside the container, so http://localhost/css/app.css and APP_URL-based links resolve to the container, not your app. Use assets() with matching filenames, absolute public URLs, or base_url on the Chromium module. This is the single most common silent failure — CSS missing, fonts wrong, images broken.

Tune the HTTP timeout, not just the queue timeout. Guzzle's default is often too short for large LibreOffice conversions, and the resulting ConnectException looks like a network problem rather than a slow render. Set a client-level timeout above your worst-case conversion and keep it below your queue worker's retry_after.

Reach for the failure flags when correctness matters. failOnHttpStatusCodes() (returns 409 on a bad main-page status) and, since v2.9, failOnResourceHttpStatusCodes() / failOnResourceLoadingFailed() turn "PDF generated but missing a stylesheet" into a loud error. Conversely, skipNetworkIdleEvent() noticeably speeds up pages with polling or long-lived connections — it's the difference between a fast render and a 30-second wait.

There are no Laravel dependents to borrow from. With essentially zero downstream packages, examples and StackOverflow answers are thin, and the README's "head to gotenberg.dev" is genuine advice rather than filler. Budget time to read the server docs directly; the method names in the PHP client map almost one-to-one to form fields, so it transfers easily. The Symfony bundle (sensiolabs/GotenbergBundle) is the closest architectural reference if you want to see how someone else structured the integration.

Testing. Don't Http::fake() this — it's PSR-18, not Laravel's HTTP client. Inject a stub ClientInterface returning a canned PSR-7 response, assert on the request body ((string) $request->getBody() contains your form fields), and keep one integration test in CI against a real Gotenberg container to catch version drift.

Screenshots are a sleeper feature. Gotenberg::chromium($url)->screenshot()->url(...) with png()/jpeg()->quality(100)/webp() gives you OG images, thumbnails, and visual-regression snapshots for free — no headless-browser plumbing in your app.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource