Failure Report
Ketika import tidak dapat menerima sebuah row, proses tidak berhenti. Row tersebut dikumpulkan bersama alasan penolakannya, lalu pada akhir proses seluruh row yang ditolak ditulis ke CSV — isi row tetap seperti semula ditambah kolom Error. File inilah yang disebut failure report, dan mekanisme ini membuat partial import menjadi hasil yang dapat diterima, bukan sebuah kegagalan total.
Gunakan halaman ini untuk memahami apa yang masuk ke report, siapa yang boleh mendownload-nya, dan bagaimana report yang sudah diperbaiki dapat di-upload kembali.
Contoh minimal yang berfungsi
use App\Panels\Admin\Resources\Users\Imports\UserImporter;
use PandaPanel\Actions\Imports\ImportRun;
$path = storage_path('app/private/panel-imports/people.csv');
$result = ImportRun::run(
UserImporter::class,
$path,
ImportRun::guessMapping(UserImporter::class, ImportRun::headings($path)),
$user->getKey(),
);
// ['imported' => 2, 'failed' => 1, 'report' => 'failed-rows-2026-08-15-114233.csv']2
3
4
5
6
7
8
9
10
11
12
13
Jika file sumber berisi:
name,email,is_admin
Grace Hopper,grace@example.test,yes
Broken,not-an-email,no
Alan Turing,alan@example.test,2
3
4
dua row akan berhasil diimport dan report berisi:
name,email,is_admin,Error
Broken,not-an-email,no,The email field must be a valid email address.2
Yang dihitung sebagai kegagalan
ImportRun menjalankan setiap row di dalam transaction sendiri dan menentukan alasan penolakan. Ada dua sumber kegagalan:
| Penyebab | Alasan yang dicatat |
|---|---|
| validation gagal | seluruh pesan validator, digabung dengan spasi |
| row melempar exception | nilai getMessage() dari exception |
Selain itu tidak dianggap failure:
- row yang tersimpan dengan bersih;
- row yang membuat
Importer::resolve()mengembalikannull— row dilewati, tidak menulis apa pun, dan tetap dihitung sebagaiimported; - row dengan kolom optional yang tidak dimapping atau kosong.
Exception pada satu row ditangkap karena database constraint, mutator, atau observer yang gagal pada satu record tidak mengatakan apa pun tentang 999 record lainnya. Pesan exception ditulis ke report di samping data yang memicunya.
Transaction per row mencegah satu row buruk membatalkan row valid sebelumnya atau meninggalkan related record yang baru sempat dibuat setengah jalan.
File report
| Properti | Nilai |
|---|---|
| format | selalu CSV, apa pun format upload awal |
| disk | $importer::disk() |
| path | {$importer::directory()}/{ownerKey}/failed-rows-{Y-m-d-His}.csv |
| header | row pertama dari file sumber, ditambah Error |
| body | satu row untuk setiap failure: cell asli kemudian alasan |
| nilai yang dikembalikan | basename saja — failed-rows-2026-08-15-114233.csv |
Report selalu CSV karena file ini ditujukan untuk diperbaiki dan di-upload kembali, dan CSV dapat dibuka hampir di mana pun. Penulisannya menggunakan PandaPanel\Support\Spreadsheet\Csv, sehingga report membawa byte-order mark dan setiap cell dinetralisasi terhadap formula injection — selalu menggunakan default Csv::write(), karena importer tidak memiliki padanan Exporter::escapesFormulas() untuk menonaktifkannya.
Header diambil dari file upload, bukan dari nama kolom importer. Hal ini membuat report menjadi salinan yang dapat dikoreksi dari file asli: kolom tetap berada pada posisi yang sudah diketahui mapping, sehingga file dapat di-upload kembali tanpa mengatur ulang mapping. Header secara literal adalah row pertama file — row yang dilewati reader sebagai heading — sehingga file yang langsung dimulai dengan data tanpa heading akan menjadikan record pertama sebagai header report dan record tersebut tidak muncul sebagai row data di report.
Jika tidak ada row yang gagal, ImportRun::run() mengembalikan 'report' => null dan tidak menulis file apa pun.
Download report
GET {panel}/imports/{file}?importer=App\Panels\Admin\…\UserImporter
route name: panel.{panelId}.import-file2
PandaPanel\Http\Controllers\PanelImportController menangani route ini dengan aturan yang sama seperti download export:
- request tanpa authenticated user mendapat 403;
fileyang kosong atau mengandung/,\, atau..mendapat 404 — request hanya boleh menyebut nama file, bukan path;importeryang bukan subclassPandaPanel\Actions\Imports\Importermendapat 404;- segmen directory dibangun dari
$request->user()->getAuthIdentifier(), sehingga hanya report milik pengguna tersebut yang dapat dijangkau; - file yang tidak ada mendapat 404, selain itu response menggunakan
Storage::disk($importer::disk())->download($path, $file).
Failure report berisi salinan data yang dicoba seseorang untuk diimport, sehingga tingkat perlindungannya harus sama dengan file export.
Cara pengguna mendapatkan report
URL dibangun untuk pengguna lalu ditempelkan ke mekanisme yang memberi tahu bahwa import selesai.
| Jalur | Yang diterima pengguna |
|---|---|
| inline, tidak ada failure | toast success, dan tanpa notification |
| inline, ada failure | toast warning dengan link Download failed rows, dan notification persistent import-finished dengan link yang sama |
| queued, tidak ada failure | notification persistent success |
| queued, ada failure | notification persistent warning dengan link report |
Inline import yang bersih cukup dijawab dengan toast. Notification center yang penuh dengan "imported 40 rows" akan menjadi tempat yang tidak lagi diperhatikan pengguna. Lihat Notification import dan export.
Memperbaiki dan meng-upload kembali
Report memang didesain untuk diedit lalu langsung di-upload ulang. Agar alur ini aman, row yang sudah pernah diimport harus meng-update record yang sama, bukan membuat record kedua. Itulah fungsi Importer::resolve():
use Illuminate\Database\Eloquent\Model;
/**
* @param array<string, mixed> $data
*/
public static function resolve(array $data): ?Model
{
$email = $data['email'] ?? null;
if (! is_string($email) || $email === '') {
return null;
}
return User::query()->where('email', $email)->first() ?? new User;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
Dengan pola tersebut, workflow-nya adalah: import, download report, perbaiki row yang disebut report, lalu upload report itu sendiri. Row yang sudah berhasil sebelumnya tidak ada di report, sementara row yang sebelumnya hanya sebagian benar akan di-update pada record yang sama.
Kolom tambahan Error tidak perlu dihapus. Karena kolom tersebut tidak dideklarasikan importer, ia diabaikan kecuali pengguna secara eksplisit memapping sebuah ImportColumn ke posisi itu — sama seperti kolom lain yang tidak dikenal importer.
Menulis alasan kegagalan sendiri
Teks alasan berasal dari validator atau exception, sehingga keduanya dapat Anda bentuk.
Custom validation message menggunakan mekanisme Laravel biasa — custom rule object atau Rule yang menyediakan pesannya sendiri:
use Illuminate\Validation\Rule;
ImportColumn::make('status')
->required()
->rules([Rule::in(['draft', 'published', 'archived'])]);
// "The selected status is invalid."2
3
4
5
6
Pesan exception masuk ke report tanpa perubahan, sehingga melempar exception dengan pesan spesifik adalah cara paling langsung untuk menjelaskan kasus bisnis tertentu:
public static function resolve(array $data): ?Model
{
$product = Product::query()->where('sku', $data['sku'])->first();
if ($product !== null && $product->isLocked()) {
throw new RuntimeException('This product is locked and cannot be updated by import.');
}
return $product ?? new Product;
}2
3
4
5
6
7
8
9
10
Karena row berjalan di dalam transaction, exception juga me-roll back seluruh perubahan yang sudah dibuat oleh row tersebut — termasuk related record yang mungkin baru saja dibuat.
Catatan
- Report ditulis satu kali pada akhir proses, menggunakan daftar failure yang ditahan di memory. Pembacaan file memang streaming satu row per satu waktu, tetapi kumpulan failure tidak memiliki batas; file yang seluruh row-nya gagal akan menahan seluruh row tersebut di memory sebelum report ditulis.
- Nomor row tidak disertakan di report. Yang disimpan adalah data row itu sendiri, karena itulah yang perlu diperbaiki. Jika source line penting, tambahkan kolom nomor ke file dan deklarasikan pada importer.
- Nama kolom alasan selalu
Error, dan tidak tersedia setter untuk mengubahnya. - Report tidak pernah dihapus otomatis. File terus terakumulasi di
{directory}/{owner}/sampai aplikasi menghapusnya — lihat Storage dan cleanup. - Report bukan kelanjutan dari import sebelumnya. Meng-upload report menjalankan import baru dengan mapping dan validation baru; tidak ada state dari proses sebelumnya yang diingat.
- Failure report tidak dibuat ketika file sama sekali tidak dapat dibaca. Kondisi tersebut menghasilkan
SpreadsheetException, menggagalkan seluruh import, dan dilaporkan sebagai satu pesan, bukan sebagai row failure.
Lihat juga
- Class importer —
resolve(),rules(),completedMessage() - Kolom dan mapping — rules dan casting per kolom
- ImportAction
- Queued import
- Notification import dan export
- CSV dan XLSX
- Storage dan cleanup
- Validation form