<?php
namespace App\Console\Commands;
use App\Exports\MultiSheetHeaderDetailExport;
use App\Exports\SingleSheetExport;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\File;
use Maatwebsite\Excel\Facades\Excel;
class ExportHeaderDetail extends Command
{
protected $signature = 'export:header-detail
{file : The output file path (e.g., storage/app/export.xlsx).}
{--entities= : Map types to Models/relations (e.g., header:JournalHeader,detail:details).}
{--key= : The primary key field of the parent model.}
{--ids= : Optional comma-separated list of parent record keys to export.}
{--query= : Eloquent query conditions to filter parent records (e.g., "status:active;totalAmount:>:10000").}
{--embed : Export from a NoSQL model with embedded arrays.}
{--sheet-title-as= : Group each parent record onto its own sheet, named by this field.}';
protected $description = 'Exports database records into a structured header/detail Excel file.';
public function handle()
{
$options = $this->getValidatedOptions();
if (!$options) return 1;
$parentModelClass = $options['entityMappings']['header'];
$query = $parentModelClass::query();
if ($options['ids']) {
$query->whereIn($options['keyField'], explode(',', $options['ids']));
}
if ($options['query']) {
try {
$this->applyQueryFilters($query, $options['query']);
} catch (\Exception $e) {
$this->error("Invalid query format: " . $e->getMessage());
return 1;
}
}
$parents = $query->get();
if ($parents->isEmpty()) {
$this->warn('No records found matching the criteria. No file was created.');
return 0;
}
$this->info("Found {$parents->count()} parent records to export. Generating file...");
if ($options['sheetTitleAs']) {
$export = new MultiSheetHeaderDetailExport($parents, $options);
} else {
$exportData = $this->buildSingleSheetData($parents, $options);
$export = new SingleSheetExport($exportData, 'Export');
}
File::ensureDirectoryExists(dirname($options['filePath']));
Excel::store($export, $options['filePath']);
$this->info("Export completed successfully! File saved to: {$options['filePath']}");
return 0;
}
private function applyQueryFilters(Builder $query, string $queryString): void
{
$conditions = explode(';', $queryString);
foreach ($conditions as $condition) {
if (empty(trim($condition))) continue;
$parts = explode(':', $condition, 3);
if (count($parts) === 2) {
list($field, $value) = $parts;
$operator = '=';
} elseif (count($parts) === 3) {
list($field, $operator, $value) = $parts;
} else {
throw new \Exception("Malformed condition '{$condition}'. Use 'field:value' or 'field:operator:value'.");
}
$field = trim($field);
$operator = trim($operator);
if (strtolower($operator) === 'in') {
$query->whereIn($field, explode(',', $value));
} elseif (strtolower($operator) === 'notin') {
$query->whereNotIn($field, explode(',', $value));
} else {
$query->where($field, $operator, $value);
}
}
}
private function buildSingleSheetData($parents, array $options): array
{
$exportData = [];
$headerEntityType = array_search($options['entityMappings']['header'], $options['entityMappings']);
foreach ($parents as $parent) {
$parentData = $parent->toArray();
$exportData[] = [$headerEntityType];
$exportData[] = array_keys($parentData);
$exportData[] = array_values($parentData);
$exportData[] = [];
foreach ($options['entityMappings'] as $type => $modelOrKey) {
if ($type === 'header' || $type === 'main') continue;
$children = $options['embed'] ? ($parent->{$type} ?? []) : $parent->{$type};
if (!empty($children) && count($children) > 0) {
$firstChild = is_array($children) ? $children[0] : $children->first()->toArray();
$exportData[] = [$type];
$exportData[] = array_keys($firstChild);
foreach ($children as $child) {
$exportData[] = is_array($child) ? array_values($child) : array_values($child->toArray());
}
$exportData[] = [];
}
}
}
return $exportData;
}
private function getValidatedOptions(): ?array
{
$filePath = $this->argument('file');
$keyField = $this->option('key');
$entities = $this->option('entities');
if (!$keyField || !$entities) {
$this->error('The --key and --entities options are required.');
return null;
}
return [
'filePath' => $filePath, 'keyField' => $keyField, 'ids' => $this->option('ids'),
'query' => $this->option('query'), 'embed' => $this->option('embed'),
'sheetTitleAs' => $this->option('sheet-title-as'),
'entityMappings' => $this->parseEntityMappings($entities),
];
}
private function parseEntityMappings(string $entities): array
{
$mappings = [];
$pairs = explode(',', $entities);
foreach ($pairs as $pair) {
list($key, $modelOrRelation) = explode(':', $pair);
$key = strtolower(trim($key));
$modelOrRelation = trim($modelOrRelation);
if ($key === 'header' || $key === 'main') {
$mappings[$key] = 'App\\Models\\' . $modelOrRelation;
} else {
$mappings[$key] = $modelOrRelation;
}
}
if (isset($mappings['header'])) $mappings['main'] = $mappings['header'];
return $mappings;
}
}