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

Pdf Merge Laravel Package

jurosh/pdf-merge

jurosh/pdf-merge is a Laravel/PHP package for combining multiple PDF files into a single document. It offers a simple API for merging existing PDFs programmatically, with Laravel integration and lightweight FPDI/FPDF-based handling for common PDF merge tasks.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require jurosh/pdf-merge

There is nothing to publish, no service provider, no facade, no config file. The package is a single class — Jurosh\PDFMerge\PDFMerger — that wraps setasign/fpdi + setasign/fpdf. Two files are worth reading before you write a line of app code:

  • vendor/jurosh/php-pdf-merge/src/PDFMerger.php — the whole API surface (note the vendor directory name differs from the Composer package name).
  • composer show setasign/fpdi setasign/fpdf — the actual parser versions you inherited. The package's last tag is from 2018, so the FPDI version resolved on your PHP may differ from what the README was written against, and that is what determines which PDFs you can actually read.

Minimal flow:

use Jurosh\PDFMerge\PDFMerger;

$pdf = new PDFMerger;

$pdf->addPDF(storage_path('app/invoices/1001.pdf'), 'all')
    ->addPDF(storage_path('app/terms/terms.pdf'), '1-2')
    ->merge('file', storage_path('app/merged/1001.pdf'));

addPDF($path, $pages) returns $this, so it chains. $pages is a 1-based string: 'all', '3', '1-3', or '1, 3, 5'. merge($mode, $path) takes one of file, string, download, browser.

First use case: an invoice rendered by barryvdh/laravel-dompdf (which returns a string), merged with a static terms-and-conditions PDF, streamed to the browser as a single Content-Disposition: attachment response. That covers 90% of what people actually use this library for.

Implementation Patterns

Controller: merge, then hand the bytes to Laravel. Use string mode so Laravel owns the response, headers, and middleware:

public function download(Invoice $invoice)
{
    $merger = new PDFMerger;
    $merger->addPDF(Pdf::loadView('invoices.pdf', compact('invoice'))->output(), 'all') // needs a real path, see gotchas
        ->addPDF(storage_path('app/terms.pdf'), 'all');

    $bytes = $merger->merge('string', '');

    return response($bytes, 200, [
        'Content-Type'        => 'application/pdf',
        'Content-Disposition' => 'attachment; filename="invoice-'.$invoice->number.'.pdf"',
    ]);
}

Wrap it in a small application service and bind it as transient. The class is stateful — addPDF accumulates onto an internal FPDI instance — so never register it as a singleton. If you bind it at all, use $this->app->bind(PDFMerger::class), or better, wrap it in your own PdfAssembler that news up a fresh merger per assemble() call. That wrapper is also the right seam for logging, retries, and swapping the backend later.

Persist with file mode to a temp path, then move it into storage. FPDF only writes to the local filesystem; it knows nothing about S3 or Flysystem. Generate into storage_path('app/tmp/'.Str::uuid().'.pdf'), then Storage::disk('s3')->putFileAs(...) and unlink() the temp file in a finally.

Push bulk work into a queued job. Merging is CPU- and memory-bound and fully synchronous. A job that builds a 50-document packet should set_time_limit(0), raise ini_set('memory_limit', '512M'), and write to a unique path per attempt so retries don't collide.

Compose documents page-by-page. Because addPDF can be called repeatedly with different page specs, the same source can be included multiple times — useful for cover pages, duplicate signature pages, or extracting a subset. Treat the chain of addPDF calls as an ordered assembly list, and consider driving it from an array of [$path, $pages] tuples so the composition is data, not code.

Attachments. merge('string', '') plus $message->attachData($bytes, 'packet.pdf', ['mime' => 'application/pdf']) avoids touching the disk entirely for small packets.

Testing. Don't snapshot the binary. Assert str_starts_with($bytes, '%PDF-') and, if the page count matters, re-read the output with FPDI (setSourceFile returns the page count) or qpdf --show-npages. Keep a couple of fixture PDFs (one generated by Dompdf, one by a real-world scanner) because parser compatibility is the thing that actually breaks.

Gotchas and Tips

Never call merge('browser', ...) or merge('download', ...) from a controller. Those modes delegate to FPDF's Output('I'|'D'), which emits raw header() calls and echoes the PDF body. In Laravel you'll get "headers already sent", broken middleware, and a response object that appends its own content after the binary. Always use string or file and let Laravel build the response.

addPDF needs a real, existing local file path — it checks file_exists() relative to the current working directory, not the Laravel base path. Always pass storage_path(...), Storage::path(...), or an absolute temp path. For a Dompdf string you must write it to a temp file first; there is no "add from string" method.

FPDI is the real constraint, not this package. It fails on encrypted PDFs, some linearized files, and PDFs using structures its parser version doesn't understand. Failures are generic (Cannot find any page in the document, Unable to find PDF object) and don't tell you which input was bad. Wrap each addPDF in a try/catch that logs the path, so when a customer uploads a weird PDF you know which one.

Pages are 1-based, and passing 0 or a page beyond the count throws. If you're slicing based on user input, clamp against setSourceFile()'s return value before calling addPDF.

Memory scales with total page count, because FPDF holds the entire output document in memory. A 200-page merge of scanned pages at 300 DPI will hit a 128M limit quickly. Set memory_limit explicitly in the job, and prefer pdfunite/qpdf via Symfony\Component\Process\Process for genuinely large documents — they stream and are dramatically faster.

The package is effectively unmaintained (last release 2018). Expect Composer conflicts if anything else in your app pins a newer setasign/fpdi, and expect deprecation notices on PHP 8.1+ from FPDF's older code. Pin the resolved versions in composer.lock, run composer audit, and isolate the dependency behind your own wrapper class so replacing it later is a one-file change.

Same output path + concurrent requests = corrupt PDF. Two requests writing storage/app/merged/out.pdf simultaneously will interleave. Use UUID filenames and clean up on a schedule.

Extension points are thin. PDFMerger exposes no hooks and keeps its FPDI instance private. If you need watermarks, page numbers, or stamps, don't subclass and fight it — merge first, then run a second pass with FPDI directly (import the merged pages as templates and draw on them), or use setasign/fpdi on its own from the start. The wrapper's value is the addPDF/merge convenience, not extensibility.

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.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource