DOCX-to-PDF in PHP splits into two camps: shell out to LibreOffice (high fidelity, needs the binary) or use a pure-PHP rendering library like PhpWord+Dompdf (zero dependencies, lower fidelity). LibreOffice is the only way to get accurate font substitution and complex table layouts. Pure-PHP is fine for simple documents but falls apart on anything with multi-column layouts or embedded objects. The API is the right call when you can't install LibreOffice.

Method 1: LibreOffice headless (highest fidelity)

LibreOffice converts DOCX to PDF with near-Word fidelity. It handles complex tables, footnotes, headers/footers, and embedded images correctly.

# Check LibreOffice is installed
soffice --version
# Install on Ubuntu/Debian
apt install libreoffice
&1";
    $output = shell_exec($cmd);

    // Clean up temp HOME
    shell_exec("rm -rf " . escapeshellarg($tmpHome));

    $basename = pathinfo($docxPath, PATHINFO_FILENAME);
    $pdfPath = rtrim($outDir, '/') . '/' . $basename . '.pdf';

    if (!file_exists($pdfPath)) {
        throw new RuntimeException("LibreOffice conversion failed: {$output}");
    }
    return $pdfPath;
}

$pdf = docxToPdf('report.docx', './output');
echo "PDF: {$pdf}\n";

The unique HOME trick is critical for concurrent requests. LibreOffice uses a lock file in ~/.config/libreoffice — two simultaneous conversions with the same HOME will deadlock. Using a per-request temp directory prevents this.

Method 2: PhpWord + Dompdf (pure PHP, lower fidelity)

PhpWord reads DOCX and can output HTML, which Dompdf renders to PDF. No binary dependencies — but complex layouts often break.

composer require phpoffice/phpword dompdf/dompdf
save($outPath);
}

docxToPdfPhpWord('simple-doc.docx', './output/result.pdf');
echo "Done\n";

PhpWord+Dompdf works well for simple text documents. It breaks on: complex tables, custom fonts not embedded in the DOCX, multi-column layouts, text boxes, and most drawing objects. Use LibreOffice or the API for anything customer-facing.

Method 3: ChangeThisFile API (Guzzle, no installs)

The ChangeThisFile API runs LibreOffice server-side. Source format is auto-detected from the filename — just pass target=pdf. Free tier: 1,000 conversions/month, no card.

composer require guzzlehttp/guzzle
 120]);
    $response = $client->post('https://changethisfile.com/v1/convert', [
        'headers' => ['Authorization' => 'Bearer ' . $apiKey],
        'multipart' => [
            ['name' => 'file', 'contents' => fopen($docxPath, 'r'), 'filename' => basename($docxPath)],
            ['name' => 'target', 'contents' => 'pdf'],
        ],
    ]);

    file_put_contents($outPath, $response->getBody()->getContents());
}

docxToPdfApi('report.docx', './output/report.pdf', 'ctf_sk_your_key_here');
echo "Done\n";

Or with PHP's built-in curl (no Guzzle needed):

 true,
        CURLOPT_TIMEOUT => 120,
        CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => [
            'file' => new CURLFile($docxPath, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', basename($docxPath)),
            'target' => 'pdf',
        ],
    ]);
    $result = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    if ($httpCode !== 200) {
        throw new RuntimeException("API error {$httpCode}: " . substr($result, 0, 200));
    }
    file_put_contents($outPath, $result);
}

When to use each

ApproachBest forTradeoff
LibreOffice headlessComplex DOCX with tables, images, custom fontsNeeds LibreOffice installed; one conversion at a time per HOME
PhpWord + DompdfSimple text documents, no binary dependenciesPoor fidelity on complex layouts; won't handle embedded objects
ChangeThisFile APINo server installs, shared hosting, serverlessNetwork call, 25MB limit on free tier

Production tips

  • Use a unique HOME per conversion. LibreOffice's lock file causes deadlocks under concurrency. Create /tmp/lo_UNIQUE before each conversion and clean up after.
  • Set exec timeouts. A large DOCX can take 30+ seconds. Use set_time_limit(0) and pass a timeout to your process management.
  • Validate the output PDF exists. LibreOffice sometimes exits 0 even on partial failures. Always check file_exists() on the expected output path.
  • Sanitize upload filenames. Never pass user-controlled strings to shell commands. Use escapeshellarg() on every argument, always.
  • Consider a job queue for high volume. LibreOffice conversion is CPU-heavy. For more than a few concurrent users, queue conversions with Redis+Resque or a simple DB queue rather than processing synchronously.

LibreOffice headless is the right default for any server-side DOCX-to-PDF pipeline — it's the same engine Word alternatives use. For shared hosting or Lambda-style environments, the API skips the binary dependency entirely. Free tier: 1,000 conversions/month.