Back to overview

Generating and Archiving PDF Invoices

Developer Tutorials Published on 13.01.2026

Generating and Archiving PDF Invoices

A hybrid approach using client-side generation with pdfMake and server-side archiving in PHP.


The Challenge

Generating PDFs in web applications often poses a choice: server-side (e.g., TCPDF, Dompdf) or client-side. Server-side rendering ensures consistency but puts load on the server. Client-side rendering leverages the user's device but makes saving the file to the server tricky.

In this tutorial, we demonstrate our hybrid solution: We use pdfMake in the browser to render complex layouts (including charts or dynamic tables) and immediately upload the generated PDF Blob to our PHP backend for archiving and emailing.

1 The PHP Upload Handler

First, we need an endpoint to receive the PDF file. In our script rechnung_pdf.php, we handle a specific action parameter save_pdf. We ensure the target directory exists and use a secure naming convention based on the invoice number.

// rechnung_pdf.php

// Check for the specific upload action
if (isset($_GET['action']) && $_GET['action'] === 'save_pdf') {
    
    // 1. Basic Validation
    if (empty($_FILES) && empty($_POST) && isset($_SERVER['CONTENT_LENGTH']) && $_SERVER['CONTENT_LENGTH'] > 0) {
        echo json_encode(['success' => false, 'error' => 'File too large (post_max_size exceeded).']);
        exit();
    }

    if (isset($_FILES['pdf_blob']) && $_FILES['pdf_blob']['error'] === UPLOAD_ERR_OK) {
        // 2. Determine Target Directory
        $user_team = $_SESSION['user']['team'];
        $clean_team = preg_replace('/[^a-zA-Z0-9_]/', '', $user_team);
        $target_dir = __DIR__ . '/files/' . $clean_team . '/Rechnungen/pdf';
        
        if (!is_dir($target_dir)) mkdir($target_dir, 0777, true);
        
        // 3. Generate Filename
        $nummer = preg_replace('/[^a-zA-Z0-9_-]/', '', $main_record['nummer'] ?? '0');
        $filename = 'auftrag' . $nummer . '_' . date('Ymd') . '.pdf';
        $target_file = $target_dir . '/' . $filename;
        
        // 4. Save File
        if (move_uploaded_file($_FILES['pdf_blob']['tmp_name'], $target_file)) {
            echo json_encode(['success' => true, 'file' => $filename]);
        } else {
            echo json_encode(['success' => false, 'error' => 'Could not move file.']);
        }
    }
    exit();
}

2 Client-Side Generation & Upload

On the client side, we define the document structure using pdfMake's JSON syntax. Instead of just downloading it to the user's computer, we generate a binary Blob and send it via AJAX (Fetch API) to our PHP handler.

// JavaScript

// 1. Define document content (simplified)
const docDefinition = {
    content: [
        { text: 'Invoice', style: 'header' },
        {
            table: {
                body: [ ['Item', 'Price'], ['Service A', '100.00 €'] ]
            }
        }
    ]
};

// 2. Create PDF Generator
const pdfDocGenerator = pdfMake.createPdf(docDefinition);

// 3. Open in browser for the user to see immediately
pdfDocGenerator.open();

// 4. Upload to server in the background
pdfDocGenerator.getBlob((blob) => {
    const formData = new FormData();
    formData.append('pdf_blob', blob, 'invoice.pdf');

    // Construct URL (reusing current script URL with action parameter)
    const uploadUrl = new URL(window.location.href);
    uploadUrl.searchParams.set('action', 'save_pdf');

    fetch(uploadUrl, { 
        method: 'POST', 
        body: formData 
    })
    .then(response => response.json())
    .then(data => {
        if (data.success) {
            console.log('PDF archived successfully:', data.file);
            // Trigger next steps (e.g., XML generation, Email)
        } else {
            console.error('Upload failed:', data.error);
        }
    });
});

3 Security Considerations

Allowing file uploads requires strict security. In our implementation, we use a HMAC signature.

  • When the user opens the invoice page, PHP generates a signature based on the Record ID and a secret server key: hash_hmac('sha256', $id, SECRET_KEY).
  • This signature is passed in the URL (?sig=...).
  • The upload handler verifies this signature before processing the file.

This ensures that even if someone guesses the URL structure, they cannot upload files without the valid cryptographic signature for that specific invoice record.