Skip to content

Run compat-legacy in production

The adapter is safe to run in Hypertext Transfer Protocol (HTTP) handlers, queue workers, and long-running processes. It is safer than legacy TCPDF 6.2.13 because it removes the two production hazards you are most likely to hit: direct output to the buffer, and die() on error. Use this page to operate it correctly.

Before production, complete the strict-mode audit in /integrations/tcpdf-compat/migration/ and deploy with strict mode off.

Legacy TCPDF Output() writes directly to the active output buffer. That can corrupt responses in HTTP frameworks and break queue workers. The adapter routes output through a safe destination bridge instead.

Choose the destination that matches the caller:

ContextDestinationWhy
Queue worker writing to storageOutput($path, 'F')Writes the file and returns an empty string. It does not interact with the buffer.
Generate then attach/uploadOutput($name, 'S')Returns the Portable Document Format (PDF) bytes; you control where they go.
Email attachmentOutput($name, 'E')Returns a base64 Multipurpose Internet Mail Extensions (MIME) body with Content-Type: application/pdf.
HTTP response you controlOutput($name, 'S')Gets bytes, then you set your own headers and body.
examples/production-worker.php
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use NextPDF\Compat\Tcpdf\Exception\TcpdfNotImplementedException;
use NextPDF\Compat\Tcpdf\TCPDF;
/**
* Render an invoice in a queue worker. Returns the storage path.
*
* @throws \RuntimeException on a render failure (Error() throws, not die()).
*/
function renderInvoiceJob(array $invoice, string $storageDir): string
{
$pdf = new TCPDF('P', 'mm', 'A4');
$pdf->SetFont('helvetica', '', 12);
$pdf->AddPage();
$pdf->Cell(0, 10, 'Invoice ' . $invoice['number'], 0, 1);
$path = $storageDir . '/invoice-' . $invoice['number'] . '.pdf';
try {
$pdf->Output($path, 'F'); // writes file, no buffer pollution
} catch (TcpdfNotImplementedException $e) {
// Only reachable if strict mode is on — it must NOT be in production.
throw new \RuntimeException('Adapter strict-mode gap in production: ' . $e->getMessage(), 0, $e);
} catch (\RuntimeException $e) {
// Error() throws RuntimeException instead of die().
throw new \RuntimeException('PDF render failed: ' . $e->getMessage(), 0, $e);
}
return $path;
}

In an HTTP handler, prefer 'S' and set headers yourself:

examples/production-http.php
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use NextPDF\Compat\Tcpdf\TCPDF;
$pdf = new TCPDF();
$pdf->AddPage();
$pdf->SetFont('helvetica', '', 12);
$pdf->Cell(0, 10, 'Report');
$bytes = $pdf->Output('report.pdf', 'S');
header('Content-Type: application/pdf');
header('Content-Length: ' . strlen($bytes));
header('Content-Disposition: inline; filename="report.pdf"');
echo $bytes;

Error() throws RuntimeException; it never calls die(). This is the most important operational change from legacy TCPDF.

  • Wrap every render entry point in try/catch.
  • Map the exception to your application’s error contract, such as HTTP 5xx, failed job, retry, or dead-letter.
  • Do not assume the process ends on a render failure; it does not.

A TcpdfNotImplementedException in a periodic strict-mode continuous integration (CI) job (recommended) is a real finding. It means a code path relies on an unsupported TCPDF parameter. Treat it as a migration work item, not a flaky test.

  • The document builds its PDF bytes lazily on the first output call. Close() is optional; calling it caches the bytes. Open() is a safe no-op.
  • endPage() does nothing because NextPDF manages page lifecycle. Remove it from hot loops; it adds no value.
  • Let PHP garbage-collect the adapter between jobs. _destroy() resets the adapter’s cached data, but you do not need to call it explicitly in normal worker loops.
  • Construct a fresh adapter per document. Do not reuse one adapter instance across unrelated documents in a long-running worker; the document state is per-instance.
  • The adapter is a thin delegation layer; the engine dominates the cost, not the adapter.
  • Define legacy constants once at boot. LegacyDefaults::register() and LegacyBootstrap::enableAliases() are idempotent and guarded, so repeated calls are cheap. Defining constants per request wastes work.
  • Prefer Output(..., 'S') or 'F' over 'I'/'D' in non-browser contexts. The inline/download paths produce framework-agnostic output that you usually do not want in a worker.
  • For high-volume generation, profile the engine, not the adapter. The per-page budget for the adapter’s own overhead is small relative to rendering.
  • Each adapter instance is independent and holds its own document state. Concurrency at the process or worker level is safe when each unit of work uses its own adapter instance.
  • The idempotency guards in LegacyBootstrap and LegacyDefaults use process-local static state; they are safe under typical PHP per-request/per-worker models. They are not designed for sharing mutable state across threads.
  • Strict-mode audit complete; production runs with strict mode off.
  • All render entry points wrapped in try/catch for RuntimeException (no reliance on die()).
  • Workers use Output(..., 'F') or 'S', never the inline path.
  • Legacy constants defined once at boot, before first construction.
  • A periodic strict-mode CI job is in place to catch regressions.
  • Byte-level test assertions are re-baselined (see /integrations/tcpdf-compat/migration/).
  • /integrations/tcpdf-compat/security-and-operations/ — encryption, signing posture, and hardening
  • /integrations/tcpdf-compat/troubleshooting/ — production failure patterns and fixes
  • /integrations/tcpdf-compat/configuration/ — strict mode and constant hygiene
  • /integrations/tcpdf-compat/migration/ — the audit that must precede production