Documentation Index
Fetch the complete documentation index at: https://docs.mejik.web.id/llms.txt
Use this file to discover all available pages before exploring further.
1. Prerequisites: Models & Migrations
For this command to work, you need the corresponding Eloquent models and database tables. Here is the example setup for the JournalHeader and JournalDetail scenario.
Migrations
..._create_journal_headers_table.php
Schema::create('journal_headers', function (Blueprint $table) {
$table->id();
$table->string('journalId')->unique();
$table->string('journalDescription');
$table->date('journalDate');
$table->decimal('totalAmount', 15, 2);
$table->timestamps();
});
..._create_journal_details_table.php
Schema::create('journal_details', function (Blueprint $table) {
$table->id();
$table->string('journalId'); // This is the foreign key
$table->string('trxDescription');
$table->date('trxDate');
$table->integer('trxQty');
// ... other detail columns
$table->timestamps();
$table->foreign('journalId')->references('journalId')->on('journal_headers')->onDelete('cascade');
});
Run php artisan migrate after creating these.
Models
app/Models/JournalHeader.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class JournalHeader extends Model
{
protected $fillable = [
'journalId', 'journalDescription', 'journalDate', 'totalAmount'
];
}
app/Models/JournalDetail.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class JournalDetail extends Model
{
protected $fillable = [
'journalId', 'trxDescription', 'trxDate', 'trxQty', //... etc.
];
}
2. Complete Artisan Command Code
Create the command with php artisan make:command ImportHeaderDetail. Place the following code into the generated file at app/Console/Commands/ImportHeaderDetail.php.
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Maatwebsite\Excel\Facades\Excel;
class ImportHeaderDetail extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'import:header-detail
{file : The path to the Excel file to import.}
{--entities= : Map entity types to Models (e.g., header:ModelName,detail:DetailModel)}
{--key= : The name of the primary/foreign key column (e.g., journalId). This is required.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Parse a structured header/detail Excel file and save data to the database.';
/**
* Execute the console command.
*/
public function handle()
{
$filePath = $this->argument('file');
$keyField = $this->option('key');
$entityMappings = $this->parseEntityMappings($this->option('entities'));
// Validation for required options
if (!$keyField) {
$this->error('The --key option is required.');
return 1;
}
if (empty($entityMappings)) {
$this->error('Entity mappings are required. Use --entities=header:Model,child:Model,...');
return 1;
}
if (!file_exists($filePath)) {
$this->error("File not found at path: {$filePath}");
return 1;
}
$this->info("Starting import of {$filePath}...");
$rows = Excel::toArray(new \stdClass(), $filePath)[0];
$currentEntityType = null;
$currentHeaders = [];
$lastParentKeyValue = null;
DB::beginTransaction();
try {
foreach ($rows as $rowIndex => $row) {
// Skip completely empty rows
if (empty(array_filter($row))) {
continue;
}
$entityType = strtolower(trim($row[0] ?? ''));
// This row defines a new entity type block (e.g., 'header', 'detail', 'education')
if (array_key_exists($entityType, $entityMappings)) {
$currentEntityType = $entityType;
// The next row contains the column headers for this block
$currentHeaders = $rows[$rowIndex + 1];
$this->line("Processing new block: '{$currentEntityType}'");
continue;
}
// Skip the header row itself, as we've already stored it
if ($currentEntityType && $row === $currentHeaders) {
continue;
}
// If we are inside a block, process this row as data
if ($currentEntityType) {
$filteredHeaders = array_filter($currentHeaders, fn($h) => $h !== null);
$dataRow = array_slice($row, 0, count($filteredHeaders));
$data = array_combine($filteredHeaders, $dataRow);
$modelClass = $entityMappings[$currentEntityType];
$modelInstance = new $modelClass();
// Logic for linking child entities to the parent
if ($currentEntityType !== 'header' && $currentEntityType !== 'main') {
// If the key field is NOT explicitly provided in the data row,
// then use the key from the last header as a fallback.
if (!isset($data[$keyField]) || empty($data[$keyField])) {
if (!$lastParentKeyValue) {
throw new \Exception("Found a '{$currentEntityType}' row without a preceding 'header'/'main' row and no explicit key.");
}
// Assign the foreign key from the last saved parent
$data[$keyField] = $lastParentKeyValue;
}
// If the key IS provided in the data row, we will use that explicit value.
}
$modelInstance->fill($data);
$modelInstance->save();
// If this was a parent entity, store its key for subsequent children
if ($currentEntityType === 'header' || $currentEntityType === 'main') {
$lastParentKeyValue = $modelInstance->{$keyField};
$this->info(" Saved Header with key: {$lastParentKeyValue}");
} else {
$savedKey = $modelInstance->{$keyField};
$this->line(" - Saved '{$currentEntityType}' detail for key {$savedKey}");
}
}
}
DB::commit();
$this->info('Import completed successfully!');
return 0;
} catch (\Exception $e) {
DB::rollBack();
$this->error("An error occurred: " . $e->getMessage());
$this->error("On row " . ($rowIndex + 1) . " while processing block '{$currentEntityType}'");
return 1;
}
}
/**
* Parses the --entities option string into a usable array.
*/
private function parseEntityMappings(?string $entities): array
{
if (!$entities) return [];
$mappings = [];
$pairs = explode(',', $entities);
foreach ($pairs as $pair) {
list($key, $modelName) = explode(':', $pair);
// We assume models are in the App\Models namespace
$mappings[strtolower(trim($key))] = 'App\\Models\\' . trim($modelName);
}
// Also map 'main' to whatever 'header' is, for convenience
if (isset($mappings['header'])) {
$mappings['main'] = $mappings['header'];
}
return $mappings;
}
}
3. Usage Documentation (English)
Command Synopsis
php artisan import:header-detail <file> --entities=<mappings> --key=<key_name>
Arguments & Options
| Parameter | Description | Required | Example |
|---|
file | The relative path to the Excel (.xlsx, .csv) file to be imported. | Yes | storage/app/import_data.xlsx |
--entities | A comma-separated list that maps the entity type from Column A to your Eloquent Model names. Use header or main for the parent entity. | Yes | --entities=header:JournalHeader,detail:JournalDetail |
--key | The name of the column that serves as the Primary Key for the header and the Foreign Key for the details. | Yes | --key=journalId |
Example Excel File Structure
The importer relies on a specific structure. Column A indicates the type of data block. The row immediately following the type declaration must contain the column headers, which must match the $fillable fields in your model.
| A | B | C | D | E |
|---|
| 1 | header | journalId | journalDescription | journalDate | totalAmount |
| 2 | | J001 | Pembelian ATK | 2025-09-01 | 220000 |
| 3 | | | | | |
| 4 | detail | journalId | trxDescription | trxQty | trxUom |
| 5 | | | Bolpoint | 10 | box |
| 6 | | J001 | Pensil | 10 | box |
How this file is processed:
- Row 5 does not have
journalId specified. It will be automatically linked to the last processed header, which is J001.
- Row 6 has
journalId explicitly set to J001. The importer will use this value.
Advanced Example: Multiple Detail Types (HR Data)
The command is powerful enough to handle a main record with multiple, different types of child records. For example, a single employee can have both dependents and an education history.
Example HR Excel File:
| A | B | C | D |
|---|
| 1 | header | employeeId | name | dateOfBirth |
| 2 | | E001 | John Doe | 1990-05-15 |
| 3 | | | | |
| 4 | dependent | name | relationship | |
| 5 | | Jane Doe | Spouse | |
| 6 | | Jimmy Doe | Child | |
| 7 | | | | |
| 8 | education | institution | degree | yearCompleted |
| 9 | | State University | B.Sc. CompSci | 2012 |
Command to Import HR Data:
Assuming you have the models PersonnelInfo, Dependent, and EducationHistory:
php artisan import:header-detail storage/app/hr_data.xlsx --entities=header:PersonnelInfo,dependent:Dependent,education:EducationHistory --key=employeeId
How this is processed:
- A
PersonnelInfo record is created for John Doe (E001).
- The two subsequent
dependent rows are processed, creating Dependent records that are automatically linked to E001.
- The final
education row is processed, creating an EducationHistory record that is also automatically linked to E001.
Example Command Execution (Journal)
php artisan import:header-detail storage/app/journals.xlsx --entities=header:JournalHeader,detail:JournalDetail --key=journalId
4. Dokumentasi Penggunaan (Bahasa Indonesia)
Sintaks Perintah
php artisan import:header-detail <file> --entities=<mappings> --key=<key_name>
Argumen & Opsi
| Parameter | Deskripsi | Wajib | Contoh |
|---|
file | Path relatif menuju file Excel (.xlsx, .csv) yang akan diimpor. | Ya | storage/app/data_impor.xlsx |
--entities | Daftar yang dipisahkan koma untuk memetakan tipe entitas dari Kolom A ke nama Model Eloquent Anda. Gunakan header atau main untuk entitas induk. | Ya | --entities=header:JournalHeader,detail:JournalDetail |
--key | Nama kolom yang berfungsi sebagai Kunci Primer (Primary Key) untuk data header dan Kunci Asing (Foreign Key) untuk data detail. | Ya | --key=journalId |
Contoh Struktur File Excel
Importer ini bergantung pada struktur file yang spesifik. Kolom A menandakan tipe blok data. Baris setelah deklarasi tipe harus berisi nama-nama kolom (header), yang harus cocok dengan properti $fillable pada Model Anda.
| A | B | C | D | E |
|---|
| 1 | header | journalId | journalDescription | journalDate | totalAmount |
| 2 | | J001 | Pembelian ATK | 2025-09-01 | 220000 |
| 3 | | | | | |
| 4 | detail | journalId | trxDescription | trxQty | trxUom |
| 5 | | | Bolpoint | 10 | box |
| 6 | | J001 | Pensil | 10 | box |
Bagaimana file ini diproses:
- Baris 5 tidak memiliki nilai pada kolom
journalId. Ia akan otomatis terhubung ke header terakhir yang diproses, yaitu J001.
- Baris 6 memiliki nilai
journalId yang diisi secara eksplisit. Importer akan menggunakan nilai ini.
Contoh Lanjutan: Beberapa Tipe Detail (Data HR)
Perintah ini cukup andal untuk menangani satu data induk yang memiliki beberapa jenis data turunan yang berbeda. Sebagai contoh, satu karyawan dapat memiliki data tanggungan (dependent) dan juga riwayat pendidikan (education history).
Contoh File Excel HR:
| A | B | C | D |
|---|
| 1 | header | employeeId | name | dateOfBirth |
| 2 | | E001 | John Doe | 1990-05-15 |
| 3 | | | | |
| 4 | dependent | name | relationship | |
| 5 | | Jane Doe | Spouse | |
| 6 | | Jimmy Doe | Child | |
| 7 | | | | |
| 8 | education | institution | degree | yearCompleted |
| 9 | | State University | B.Sc. CompSci | 2012 |
Perintah untuk Impor Data HR:
Dengan asumsi Anda memiliki model PersonnelInfo, Dependent, dan EducationHistory:
php artisan import:header-detail storage/app/hr_data.xlsx --entities=header:PersonnelInfo,dependent:Dependent,education:EducationHistory --key=employeeId
Bagaimana ini diproses:
- Sebuah record
PersonnelInfo dibuat untuk John Doe (E001).
- Dua baris
dependent berikutnya diproses, membuat record Dependent yang secara otomatis terhubung ke E001.
- Baris
education terakhir diproses, membuat record EducationHistory yang juga terhubung secara otomatis ke E001.
Contoh Eksekusi Perintah (Jurnal)
php artisan import:header-detail storage/app/jurnal.xlsx --entities=header:JournalHeader,detail:JournalDetail --key=journalId