<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
use PhpOffice\PhpWord\TemplateProcessor;
class GenerateInvoiceFromTemplate extends Command
{
/**
* The name and signature of the console command.
* @var string
*/
protected $signature = 'invoice:generate
{--data=invoice_data.json : The path to the JSON data file}
{--template=storage/app/templates/invoice_template.docx : The path to the DOCX template file}
{--output=storage/app/invoices/invoice_output.docx : The path for the generated DOCX file}';
/**
* The console command description.
* @var string
*/
protected $description = 'Generate a DOCX invoice from a template and a JSON data file';
/**
* Execute the console command.
*/
public function handle()
{
// 1. Get file paths from options
$dataPath = $this->option('data');
$templatePath = $this->option('template');
$outputPath = $this->option('output');
// 2. Validate inputs
if (!File::exists($dataPath)) {
$this->error("Data file not found at: {$dataPath}");
return 1;
}
if (!File::exists($templatePath)) {
$this->error("Template file not found at: {$templatePath}");
return 1;
}
// 3. Load and parse JSON data
$data = json_decode(File::get($dataPath), true);
if (json_last_error() !== JSON_ERROR_NONE) {
$this->error("Invalid JSON in data file. Error: " . json_last_error_msg());
return 1;
}
try {
// 4. Load the DOCX template
$templateProcessor = new TemplateProcessor($templatePath);
// 5. Handle Image Replacement
if (isset($data['logo']) && !empty($data['logo'])) {
$localImagePath = $this->getLocalImagePath($data['logo'], $data['logo_is_url'] ?? false);
if ($localImagePath) {
$templateProcessor->setImageValue('logo', [
'path' => $localImagePath,
'width' => 150,
'ratio' => true
]);
// Clean up temporary image if it was downloaded
if ($data['logo_is_url'] ?? false) {
File::delete($localImagePath);
}
} else {
$this->warn("Could not process image for 'logo'. Removing placeholder.");
$templateProcessor->setValue('logo', ''); // Remove placeholder if image fails
}
}
// 6. Handle Conditional Block (Bill To)
if (isset($data['bill_present']) && $data['bill_present']) {
$templateProcessor->cloneBlock('if_bill', 1, true, false, [
['bill_name' => $data['bill_to']['name'] ?? ''],
['bill_addr1' => $data['bill_to']['addr1'] ?? ''],
['bill_addr2' => $data['bill_to']['addr2'] ?? ''],
['bill_phone' => $data['bill_to']['phone'] ?? ''],
['bill_email' => $data['bill_to']['email'] ?? ''],
]);
} else {
$templateProcessor->deleteBlock('if_bill');
}
// 7. Handle Simple Value Replacements
$templateProcessor->setValues([
'company_name' => $data['company_name'] ?? '',
'company_address' => $data['company_address'] ?? '',
'company_phone' => $data['company_phone'] ?? '',
'company_email' => $data['company_email'] ?? '',
'invoice_number' => $data['invoice_number'] ?? '',
'invoice_date' => $data['invoice_date'] ?? '',
'invoice_payment' => $data['invoice_payment'] ?? '',
'invoice_subtotal' => number_format($data['invoice_subtotal'] ?? 0, 2),
'company_tax_name' => $data['company_tax_name'] ?? 'Tax',
'invoice_tax' => number_format($data['invoice_tax'] ?? 0, 2),
'invoice_fees' => number_format($data['invoice_fees'] ?? 0, 2),
'invoice_total' => number_format($data['invoice_total'] ?? 0, 2),
'department' => $data['department'] ?? '',
'cc_list' => isset($data['cc_list']) ? implode(', ', $data['cc_list']) : ''
]);
// 8. Handle Table (Loop)
$items = $data['item_list'] ?? [];
if (!empty($items)) {
$templateProcessor->cloneRow('item.no', count($items));
foreach ($items as $index => $item) {
$rowNumber = $index + 1;
$templateProcessor->setValue('item.no#' . $rowNumber, $rowNumber);
$templateProcessor->setValue('item.name#' . $rowNumber, $item['name'] ?? '');
$templateProcessor->setValue('item.quantity#' . $rowNumber, $item['quantity'] ?? 0);
$templateProcessor->setValue('item.total#' . $rowNumber, number_format($item['total'] ?? 0, 2));
$templateProcessor->setValue('item.amount#' . $rowNumber, number_format($item['amount'] ?? 0, 2));
}
} else {
// If there are no items, you might want to remove the template row
$templateProcessor->cloneRow('item.no', 0);
}
// 9. Save the final document
File::ensureDirectoryExists(dirname($outputPath));
$templateProcessor->saveAs($outputPath);
$this->info("Invoice generated successfully at: {$outputPath}");
return 0;
} catch (\Exception $e) {
$this->error("An error occurred: " . $e->getMessage());
return 1;
}
}
/**
* Get the local file path for an image, downloading it if it's a URL.
*
* @param string $path
* @param bool $isUrl
* @return string|null
*/
private function getLocalImagePath(string $path, bool $isUrl): ?string
{
if ($isUrl) {
$contents = @file_get_contents($path);
if ($contents === false) {
$this->warn("Could not download image from URL: {$path}");
return null;
}
$tempPath = storage_path('app/temp/' . Str::random(16));
File::ensureDirectoryExists(dirname($tempPath));
File::put($tempPath, $contents);
return $tempPath;
}
if (File::exists($path)) {
return $path;
}
$this->warn("Local image file not found at: {$path}");
return null;
}
}