<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
use PhpOffice\PhpWord\IOFactory;
use PhpOffice\PhpWord\Settings;
use PhpOffice\PhpWord\TemplateProcessor;
class DocxGenerateCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'docx:generate
{--template=storage/app/templates/invoice_template.docx : The path to the DOCX template file}
{--output=storage/app/output/result : The base path for the generated file (extension is added automatically)}
{--data=invoice_data.json : The path to the JSON data file (ignored in "blade" mode)}
{--mode=merge : The output mode (merge, blade, html, pdf)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Generate DOCX, HTML, PDF, or Blade files from a DOCX template.';
/**
* Execute the console command.
*/
public function handle()
{
$mode = $this->option('mode');
$templatePath = $this->option('template');
$outputPath = $this->option('output');
$dataPath = $this->option('data');
if (!in_array($mode, ['merge', 'blade', 'html', 'pdf'])) {
$this->error("Invalid mode '{$mode}'. Available modes are: merge, blade, html, pdf.");
return 1;
}
if (!File::exists($templatePath)) {
$this->error("Template file not found at: {$templatePath}");
return 1;
}
$finalOutputPath = $this->getFinalOutputPath($outputPath, $mode);
File::ensureDirectoryExists(dirname($finalOutputPath));
try {
switch ($mode) {
case 'blade':
$this->generateBlade($templatePath, $finalOutputPath);
break;
case 'html':
case 'pdf':
case 'merge':
$this->generateFromData($templatePath, $dataPath, $finalOutputPath, $mode);
break;
}
} catch (\Exception $e) {
$this->error("An error occurred: " . $e->getMessage());
return 1;
}
return 0;
}
/**
* Handles modes that require data merging (merge, html, pdf).
*/
private function generateFromData(string $templatePath, string $dataPath, string $finalOutputPath, string $mode)
{
if (!File::exists($dataPath)) {
$this->error("Data file not found at: {$dataPath}");
throw new \Exception("Data file not found.");
}
$data = json_decode(File::get($dataPath), true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \Exception("Invalid JSON in data file: " . json_last_error_msg());
}
$templateProcessor = $this->processTemplateMerge($templatePath, $data);
if ($mode === 'merge') {
$templateProcessor->saveAs($finalOutputPath);
$this->info("DOCX file generated successfully at: {$finalOutputPath}");
return;
}
// For HTML and PDF, we need to save a temporary DOCX, then convert it.
$tempDocxPath = storage_path('app/temp/' . Str::random(16) . '.docx');
File::ensureDirectoryExists(dirname($tempDocxPath));
try {
$templateProcessor->saveAs($tempDocxPath);
$phpWord = IOFactory::load($tempDocxPath);
if ($mode === 'html') {
$writer = IOFactory::createWriter($phpWord, 'HTML');
$writer->save($finalOutputPath);
$this->info("HTML file generated successfully at: {$finalOutputPath}");
}
if ($mode === 'pdf') {
if (!class_exists(\Dompdf\Dompdf::class)) {
$this->error('PDF generation requires DomPDF. Please run: composer require dompdf/dompdf');
throw new \Exception('DomPDF library not found.');
}
Settings::setPdfRendererName(Settings::PDF_RENDERER_DOMPDF);
Settings::setPdfRendererPath('.'); // Path to vendor/dompdf/dompdf is autoloaded
$writer = IOFactory::createWriter($phpWord, 'PDF');
$writer->save($finalOutputPath);
$this->info("PDF file generated successfully at: {$finalOutputPath}");
}
} finally {
if (File::exists($tempDocxPath)) {
File::delete($tempDocxPath);
}
}
}
/**
* Generates a Blade template from a DOCX file.
*/
private function generateBlade(string $templatePath, string $finalOutputPath)
{
$this->info("Converting DOCX to Blade template...");
$phpWord = IOFactory::load($templatePath);
$htmlWriter = IOFactory::createWriter($phpWord, 'HTML');
$htmlContent = $htmlWriter->getWriterPart('Body')->write();
// Convert ${variable} to {{ $variable }}
$bladeContent = preg_replace('/\$\{(.*?)\}/', '{{ $\1 }}', $htmlContent);
// Clean up potential empty tags around the placeholders
$bladeContent = preg_replace('/<p><\/p>/', '', $bladeContent);
File::put($finalOutputPath, $bladeContent);
$this->info("Blade template created successfully at: {$finalOutputPath}");
}
/**
* Processes a DOCX template with JSON data.
* @return TemplateProcessor
*/
private function processTemplateMerge(string $templatePath, array $data): TemplateProcessor
{
$templateProcessor = new TemplateProcessor($templatePath);
// Handle images
if (isset($data['logo']) && !empty($data['logo'])) {
// ... [Image handling logic remains the same]
}
// Handle conditional blocks
if (isset($data['bill_present']) && $data['bill_present']) {
$templateProcessor->cloneBlock('if_bill', 1, true, false, $data['bill_to'] ?? []);
} else {
$templateProcessor->deleteBlock('if_bill');
}
// Handle simple values
$templateProcessor->setValues([
'company_name' => $data['company_name'] ?? '',
'company_address' => $data['company_address'] ?? '',
'invoice_number' => $data['invoice_number'] ?? '',
// ... add all other simple variables here
'invoice_total' => number_format($data['invoice_total'] ?? 0, 2),
'cc_list' => isset($data['cc_list']) ? implode(', ', $data['cc_list']) : ''
]);
// Handle table rows
$items = $data['item_list'] ?? [];
if (!empty($items)) {
$templateProcessor->cloneRowAndSetValues('item.no', $items);
// Note: cloneRowAndSetValues requires your JSON keys to match the placeholder names
// e.g., 'item.name' in DOCX must be 'name' in your JSON item object.
// We will stick to the manual loop for more control.
$templateProcessor->cloneRow('item.no', count($items));
foreach ($items as $index => $item) {
$rowNum = $index + 1;
$templateProcessor->setValue('item.no#'.$rowNum, $rowNum);
$templateProcessor->setValue('item.name#'.$rowNum, $item['name'] ?? '');
$templateProcessor->setValue('item.quantity#'.$rowNum, $item['quantity'] ?? 0);
$templateProcessor->setValue('item.amount#'.$rowNum, number_format($item['amount'] ?? 0, 2));
}
} else {
$templateProcessor->cloneRow('item.no', 0);
}
return $templateProcessor;
}
/**
* Determines the final output path with the correct extension.
*/
private function getFinalOutputPath(string $basePath, string $mode): string
{
$extensionMap = [
'merge' => '.docx',
'blade' => '.blade.php',
'html' => '.html',
'pdf' => '.pdf',
];
return $basePath . $extensionMap[$mode];
}
}