ayoratoumvone / documentgeneratorx
A package to generate documents (DOCX/PDF) based on templates with advanced variable types including images.
Requires
- php: ^8.1
- dompdf/dompdf: ^2.0
- illuminate/support: ^9.0|^10.0|^11.0|^12.0
- intervention/image: ^3.0
- phpoffice/phpword: ^1.2
Requires (Dev)
- laravel/pint: ^1.13
- orchestra/testbench: ^7.0|^8.0|^9.0
- phpunit/phpunit: ^9.0|^10.0
README
A Laravel package to generate PDF documents from DOCX or HTML templates with variable replacement, styling support, batch generation, and queue integration for parallel processing.
Features
- PDF Output: All documents are generated as high-quality PDF
- DOCX Templates: Use Microsoft Word documents as templates
- HTML Templates: Use HTML files as templates
- Variable Styling: Apply colors, bold, italic, and more to variables
- Image Support: Insert images with custom dimensions
- Array / Repeating Rows: Fill a table column from a list — rows are added automatically
- Batch Generation: Generate multiple documents at once
- Queue Support: Process documents in background with Laravel queues
- Event System: Hook into generation lifecycle with Laravel events
- LibreOffice Integration: Best quality PDF output (default)
Installation
composer require ayoratoumvone/documentgeneratorx
Publish the config file:
php artisan vendor:publish --tag="documentgenerator-config"
Quick Start
use Ayoratoumvone\Documentgeneratorx\Facades\DocumentGenerator; // Generate PDF from DOCX template $pdfPath = DocumentGenerator::template('template.docx') ->variables([ 'name' => 'John Doe', 'photo' => 'https://example.com/photo.jpg', ]) ->generate();
Table of Contents
- Variable Syntax
- Styling Variables
- Arrays & Repeating Table Rows
- Single Document Generation
- Batch Generation
- Queue & Events (Parallel Processing)
- PDF Conversion Methods
- Configuration
- Standalone Usage
Variable Syntax
Use double curly braces with type annotations in your template:
{{variable_name:type,option1:value1,option2:value2}}
Supported Types
| Type | Syntax | Example Value |
|---|---|---|
| Text | {{name:text}} |
'John Doe' |
| Number | {{age:number}} |
25 |
| Image | {{photo:image,width:200,height:100}} |
'path/to/image.jpg' |
| Date | {{date:date}} |
'2024-01-15' |
| Boolean | {{active:boolean}} |
true |
| Array | {{items:array}} |
['Hammer', 'Saw', 'Nail'] |
Array placeholders fill a table column from a list and grow the table automatically. See Arrays & Repeating Table Rows.
Image Options
{{logo:image}} // Default size
{{photo:image,width:200}} // Fixed width
{{banner:image,width:200,height:100}} // Fixed dimensions
{{avatar:image,ratio:1:1}} // Aspect ratio
Styling Variables
Apply styling directly in template placeholders:
{{title:text,color:red,bold:true}}
{{name:text,font-size:18,underline:true}}
{{warning:text,color:#FF0000,background-color:#FFFF00}}
Supported Style Properties
| Property | Example | Description |
|---|---|---|
color |
color:red or color:#FF0000 |
Text color |
bold |
bold:true |
Bold text |
italic |
italic:true |
Italic text |
underline |
underline:true |
Underlined text |
font-size |
font-size:14 |
Font size in points |
font-family |
font-family:Arial |
Font family |
background-color |
background-color:#FFFF00 |
Highlight color |
Named Colors
red, green, blue, black, white, yellow, orange, purple, pink, gray, brown, navy, teal, maroon
Arrays & Repeating Table Rows
Available since v2.0.7.
Use the array type to render a list of values down a column. The value you
pass is a plain array of strings (numbers, booleans and dates work too — they are
stringified). One value is placed per row, and the table grows by itself to
fit the data.
{{items:array}}
['items' => ['Hammer', 'Saw', 'Nail']]
How it works
Put the placeholder in one row of a table. That row becomes the template:
- It is cloned once per value in the list.
- If you drew extra blank rows under it, they are filled first; when the list is longer, new rows are added automatically. When the list is shorter, the leftover blank rows are removed — so the output is always exactly the right size.
- If the list is empty, the template row is removed (only the header remains).
Multiple columns side by side
Place an array placeholder in each column of the same row. The row is cloned to the length of the longest list; shorter columns simply leave blank cells.
DOCX template (a 4-column table):
| Numero | Nom | Q | Dimission |
|---|---|---|---|
{{nums:array}} |
{{noms:array}} |
{{qs:array}} |
{{dims:array}} |
PHP code:
DocumentGenerator::template('inventory.docx') ->variables([ 'nums' => ['1', '2', '3'], 'noms' => ['Hammer', 'Saw', 'Nail'], 'qs' => ['10', '5', '200'], 'dims' => ['20cm', '40cm', '3cm'], ]) ->generate('inventory.pdf');
Result — the single placeholder row becomes three filled rows:
| Numero | Nom | Q | Dimission |
|---|---|---|---|
| 1 | Hammer | 10 | 20cm |
| 2 | Saw | 5 | 40cm |
| 3 | Nail | 200 | 3cm |
Styling array values
Array placeholders accept the same style options as text:
{{noms:array,bold:true,color:#2c3e50}}
Arrays outside a table
If an array placeholder is not inside a table, the values are stacked on
separate lines within the same paragraph (joined with line breaks).
Notes
- Values are XML-escaped automatically (
&,<,>are safe). - A non-array value (e.g. a single string) is treated as a one-element list.
- Arrays inside a nested table (a table within a table cell) are not expanded.
Single Document Generation
Methods for generating one document at a time.
Available Methods
| Method | Description | Returns |
|---|---|---|
generate($path) |
Generate PDF to file path | File path |
download($filename) |
Generate and return download response | HTTP Response |
generateToStorage($path, $disk) |
Generate and save to Laravel storage | Storage path |
Generate to File
use Ayoratoumvone\Documentgeneratorx\Facades\DocumentGenerator; $pdfPath = DocumentGenerator::template('invoice.docx') ->variables([ 'customer_name' => 'John Doe', 'total' => '$1,234.00', ]) ->generate('invoices/invoice-001.pdf');
Download Response (Single Document Only)
// Returns a download response - works for SINGLE document only return DocumentGenerator::template('contract.docx') ->variables(['name' => 'John Doe']) ->download('contract.pdf');
Save to Storage
$path = DocumentGenerator::template('report.docx') ->variables(['title' => 'Monthly Report']) ->generateToStorage('reports/march-2024.pdf', 'public');
Batch Generation
Generate multiple documents at once. Returns an array of results.
Note:
download()is NOT available for batch generation. You cannot download multiple files in a single HTTP response. For batch downloads, generate files then create a ZIP archive.
Available Methods
| Method | Description | Returns |
|---|---|---|
generateBatch($documents) |
Generate multiple PDFs (same template) | Array of results |
batchGenerate($documents) |
Generate multiple PDFs (different templates) | Array of results |
Same Template, Different Data
use Ayoratoumvone\Documentgeneratorx\Facades\DocumentGenerator; $generator = DocumentGenerator::template('certificate.docx'); $results = $generator->generateBatch([ ['variables' => ['name' => 'Alice Johnson'], 'output' => 'certs/alice.pdf'], ['variables' => ['name' => 'Bob Smith'], 'output' => 'certs/bob.pdf'], ['variables' => ['name' => 'Carol White'], 'output' => 'certs/carol.pdf'], ]); // Check results foreach ($results as $result) { if ($result['success']) { echo "Generated: {$result['path']}\n"; } else { echo "Failed: {$result['error']}\n"; } }
Different Templates
use Ayoratoumvone\Documentgeneratorx\DocumentGenerator; $results = DocumentGenerator::batchGenerate([ [ 'template' => 'invoice.docx', 'variables' => ['customer' => 'John', 'total' => '$500'], 'output' => 'docs/invoice.pdf', ], [ 'template' => 'contract.docx', 'variables' => ['client' => 'Jane', 'date' => '2024-01-15'], 'output' => 'docs/contract.pdf', ], ]);
Progress Callback
$results = $generator->generateBatch($documents, false, function($completed, $total, $path) { $percent = round(($completed / $total) * 100); echo "Progress: {$percent}% ({$completed}/{$total})\n"; });
Download Multiple Documents as ZIP
To let users download multiple documents, create a ZIP file:
use ZipArchive; // Generate batch $results = $generator->generateBatch($documents); // Create ZIP with successful files $zipPath = storage_path('app/temp/documents.zip'); $zip = new ZipArchive(); $zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE); foreach ($results as $result) { if ($result['success']) { $zip->addFile($result['path'], basename($result['path'])); } } $zip->close(); // Return ZIP download return response()->download($zipPath, 'documents.zip')->deleteFileAfterSend(true);
Queue & Events (Parallel Processing)
For generating many documents simultaneously, use Laravel's queue system with the built-in job and events. Each document gets a system-generated documentId that you can use to track its progress.
Why Use Queues?
- Parallel Processing: Generate multiple documents at the same time
- Background Processing: Don't block web requests
- Reliability: Automatic retries on failure
- Scalability: Distribute work across multiple queue workers
- Tracking: Each document gets a unique ID for status tracking
Generate Single Document in Queue
use Ayoratoumvone\Documentgeneratorx\Jobs\GenerateDocument; // Create the job to get the document ID $job = new GenerateDocument( templatePath: storage_path('templates/invoice.docx'), variables: ['customer' => 'John Doe', 'total' => '$1,234'], outputPath: storage_path('app/invoices/invoice-001.pdf') ); // Get the document ID for tracking (store this in your database) $documentId = $job->documentId; // e.g., "doc_550e8400-e29b-41d4-a716-446655440000" // Save to your database DocumentRequest::create([ 'document_id' => $documentId, 'user_id' => auth()->id(), 'status' => 'pending', ]); // Dispatch the job dispatch($job); // Return the document ID to user so they can check status later return response()->json(['document_id' => $documentId, 'status' => 'processing']);
Generate Multiple Documents in Parallel
Use the GenerateDocumentBatch helper:
use Ayoratoumvone\Documentgeneratorx\Jobs\GenerateDocumentBatch; // Create batch $batchHelper = GenerateDocumentBatch::create([ ['template' => 'invoice.docx', 'variables' => ['name' => 'Alice'], 'output' => 'alice.pdf'], ['template' => 'invoice.docx', 'variables' => ['name' => 'Bob'], 'output' => 'bob.pdf'], ['template' => 'invoice.docx', 'variables' => ['name' => 'Carol'], 'output' => 'carol.pdf'], ]); // Get all document IDs BEFORE dispatching (store these in your database) $documentIds = $batchHelper->getDocumentIds(); // ['doc_abc123...', 'doc_def456...', 'doc_ghi789...'] // Save to your database for tracking foreach ($documentIds as $documentId) { DocumentRequest::create([ 'document_id' => $documentId, 'user_id' => auth()->id(), 'status' => 'pending', ]); } // Now dispatch $batch = $batchHelper ->name('Monthly Invoices') ->onQueue('documents') ->dispatch(); // Return document IDs to user return response()->json([ 'batch_id' => $batchHelper->getBatchId(), 'document_ids' => $documentIds, 'status' => 'processing', ]);
Tracking Document Status with Events
When documents complete (or fail), events fire with the documentId. Use listeners to update your database:
// In your EventServiceProvider protected $listen = [ \Ayoratoumvone\Documentgeneratorx\Events\DocumentGenerated::class => [ \App\Listeners\UpdateDocumentStatus::class, ], \Ayoratoumvone\Documentgeneratorx\Events\DocumentGenerationFailed::class => [ \App\Listeners\HandleDocumentFailure::class, ], ];
// app/Listeners/UpdateDocumentStatus.php class UpdateDocumentStatus { public function handle(DocumentGenerated $event): void { // Find your record using the document ID $request = DocumentRequest::where('document_id', $event->documentId)->first(); if ($request) { $request->update([ 'status' => 'completed', 'file_path' => $event->outputPath, 'completed_at' => now(), ]); // Notify the user $request->user->notify(new DocumentReadyNotification($event->outputPath)); } } }
API Endpoint Example
// Controller: Start document generation public function generateInvoice(Request $request) { $job = new GenerateDocument( 'invoice.docx', ['customer' => $request->customer_name, 'total' => $request->total], "invoices/{$request->invoice_id}.pdf" ); // Store for tracking DocumentRequest::create([ 'document_id' => $job->documentId, 'user_id' => auth()->id(), 'type' => 'invoice', 'status' => 'processing', ]); dispatch($job); return response()->json([ 'document_id' => $job->documentId, 'message' => 'Document is being generated', ]); } // Controller: Check status public function checkStatus(string $documentId) { $request = DocumentRequest::where('document_id', $documentId) ->where('user_id', auth()->id()) ->firstOrFail(); return response()->json([ 'document_id' => $documentId, 'status' => $request->status, 'file_path' => $request->file_path, 'completed_at' => $request->completed_at, ]); }
Events
The package fires events at each stage of document generation. Use these to add custom logic.
| Event | When Fired | Use Case |
|---|---|---|
DocumentGenerating |
Before generation starts | Validate, log start |
DocumentGenerated |
After successful generation | Notify user, move file |
DocumentGenerationFailed |
When generation fails | Log error, notify admin |
BatchGenerationCompleted |
When batch finishes | Zip files, send summary |
Setting Up Event Listeners
1. Publish example listeners:
php artisan vendor:publish --tag="documentgenerator-listeners"
2. Register listeners in EventServiceProvider:
// app/Providers/EventServiceProvider.php use Ayoratoumvone\Documentgeneratorx\Events\DocumentGenerated; use Ayoratoumvone\Documentgeneratorx\Events\DocumentGenerationFailed; use Ayoratoumvone\Documentgeneratorx\Events\BatchGenerationCompleted; use App\Listeners\DocumentGenerator\LogDocumentGenerated; use App\Listeners\DocumentGenerator\NotifyDocumentReady; use App\Listeners\DocumentGenerator\HandleGenerationFailed; use App\Listeners\DocumentGenerator\HandleBatchCompleted; protected $listen = [ DocumentGenerated::class => [ LogDocumentGenerated::class, NotifyDocumentReady::class, ], DocumentGenerationFailed::class => [ HandleGenerationFailed::class, ], BatchGenerationCompleted::class => [ HandleBatchCompleted::class, ], ];
Example: Notify User When Document is Ready
// app/Listeners/DocumentGenerator/NotifyDocumentReady.php namespace App\Listeners\DocumentGenerator; use Ayoratoumvone\Documentgeneratorx\Events\DocumentGenerated; use App\Models\DocumentRequest; use App\Notifications\DocumentReadyNotification; class NotifyDocumentReady { public function handle(DocumentGenerated $event): void { // Find the document request using the system-generated documentId $request = DocumentRequest::where('document_id', $event->documentId)->first(); if ($request) { // Update status $request->update([ 'status' => 'completed', 'file_path' => $event->outputPath, ]); // Notify the user who requested this document $request->user->notify(new DocumentReadyNotification( $event->outputPath, $event->getFileSizeFormatted() )); } } }
Example: Create ZIP of Batch Documents
// app/Listeners/DocumentGenerator/HandleBatchCompleted.php use Ayoratoumvone\Documentgeneratorx\Events\BatchGenerationCompleted; use ZipArchive; class HandleBatchCompleted { public function handle(BatchGenerationCompleted $event): void { if (!$event->isFullySuccessful()) { Log::warning("Batch {$event->batchId} had {$event->failedCount} failures"); return; } // Create ZIP with all documents $zipPath = storage_path("app/batches/{$event->batchId}.zip"); $zip = new ZipArchive(); $zip->open($zipPath, ZipArchive::CREATE); foreach ($event->getSuccessfulPaths() as $path) { $zip->addFile($path, basename($path)); } $zip->close(); Log::info("Batch ZIP created: {$zipPath}"); } }
Running Queue Workers
Start queue workers to process document generation jobs:
# Single worker php artisan queue:work # Multiple workers for parallel processing php artisan queue:work --queue=documents & php artisan queue:work --queue=documents & php artisan queue:work --queue=documents & # Using Supervisor (production) # See Laravel docs: https://laravel.com/docs/queues#supervisor-configuration
PDF Conversion Methods
LibreOffice (Default - Best Quality)
Uses LibreOffice for pixel-perfect PDF conversion. Recommended for production.
Install LibreOffice:
- Windows: Download
- Linux:
sudo apt install libreoffice - macOS:
brew install libreoffice
DOCUMENT_PDF_CONVERSION=libreoffice LIBREOFFICE_PATH="C:\Program Files\LibreOffice\program\soffice.exe"
Dompdf (HTML-based Fallback)
No external dependencies, but may not preserve all formatting.
DOCUMENT_PDF_CONVERSION=dompdf
Error Handling
If LibreOffice is not installed, you'll get a helpful error:
LibreOffice is not installed or not found on this system.
To fix this, you have two options:
1. Install LibreOffice (recommended for best PDF quality):
Download from: https://www.libreoffice.org/download/download/
Then set the path in your .env file:
LIBREOFFICE_PATH="C:\Program Files\LibreOffice\program\soffice.exe"
2. Use HTML-based conversion (no LibreOffice required):
Set in your .env file:
DOCUMENT_PDF_CONVERSION=dompdf
Configuration
// config/documentgenerator.php return [ // PDF conversion method: 'libreoffice' (default) or 'dompdf' 'pdf_conversion' => env('DOCUMENT_PDF_CONVERSION', 'libreoffice'), // LibreOffice executable path (auto-detected if not set) 'libreoffice_path' => env('LIBREOFFICE_PATH', null), // Default template storage path 'template_path' => storage_path('app/document-templates'), // Default output path for permanent files 'output_path' => storage_path('app/generated-documents'), // Save to temp directory (auto-deleted) 'temp_output' => true, // Delete file after download() response 'delete_after_download' => true, // Cleanup temp files on script end 'cleanup_on_shutdown' => true, // Default storage disk 'disk' => 'local', ];
Standalone Usage (Without Laravel)
require 'vendor/autoload.php'; use Ayoratoumvone\Documentgeneratorx\Generators\DocxToPdfGenerator; $generator = new DocxToPdfGenerator(); // Set LibreOffice path $generator->setLibreOfficePath('C:\Program Files\LibreOffice\program\soffice.exe'); // Generate PDF $generator->generate( 'template.docx', ['name' => 'John Doe', 'date' => '2024-01-15'], 'output.pdf' );
Requirements
- PHP 8.1+
- Laravel 9.x, 10.x, 11.x, or 12.x
- GD extension for image processing
- LibreOffice (recommended) or Dompdf
License
MIT License. See LICENSE.md for details.