Class Exporter
PandaPanel\Actions\Exports\Exporter mendefinisikan apa itu sebuah export: sekumpulan kolom, sebuah query, dan lokasi tempat file disimpan. Anda membuat satu exporter untuk setiap jenis data yang perlu diexport, lalu memberikan nama class-nya ke ExportAction.
Exporter dibuat sebagai abstract class dengan static method, bukan closure pada action, dengan alasan yang sama seperti policy dibuat sebagai class: queued export berjalan di proses yang berbeda dari request yang memintanya, dan hanya nama class yang dapat dibawa melewati batas tersebut. Semua method bersifat static karena tidak ada state antar-row yang perlu disimpan di luar query.
Contoh minimal yang berfungsi
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Resources\Users\Exports;
use PandaPanel\Actions\Exports\ExportColumn;
use PandaPanel\Actions\Exports\Exporter;
final class UserExporter extends Exporter
{
/**
* @return list<ExportColumn>
*/
public static function columns(): array
{
return [
ExportColumn::make('id')->label('ID'),
ExportColumn::make('name'),
ExportColumn::make('email'),
];
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Itu sudah merupakan exporter yang lengkap. columns() adalah satu-satunya abstract method; seluruh method lainnya memiliki default yang sudah dapat digunakan.
Semua method
| Method | Signature | Default |
|---|---|---|
columns | abstract public static function columns(): array | — wajib, list<ExportColumn> |
query | public static function query(Builder $query): Builder | query tidak diubah |
fileName | public static function fileName(): string | {kebab-class-basename}-Y-m-d-His |
disk | public static function disk(): string | 'local' |
directory | public static function directory(): string | 'panel-exports' |
formats | public static function formats(): array | [SpreadsheetFormat::Csv, SpreadsheetFormat::Xlsx] |
escapesFormulas | public static function escapesFormulas(): bool | true |
chunkSize | public static function chunkSize(): int | 500 |
queueAfter | public static function queueAfter(): int | 2000 |
completedMessage | public static function completedMessage(int $records): string | Your export of {n} records is ready. |
columns()
Kolom yang ditawarkan pada dialog, sesuai urutan penulisannya. Seluruh setter ExportColumn dibahas di Kolom dan mapping; versi singkatnya:
use PandaPanel\Actions\Exports\ExportColumn;
public static function columns(): array
{
return [
ExportColumn::make('id')->label('ID'),
ExportColumn::make('name'),
// Dot notation reads through relations with data_get().
ExportColumn::make('company.name')->label('Company'),
ExportColumn::make('is_admin')
->label('Administrator')
->formatUsing(static fn (mixed $value): string => $value ? 'Yes' : 'No'),
// Offered in the dialog but unticked.
ExportColumn::make('updated_at')->label('Last updated')->enabledByDefault(false),
];
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Dua kolom dengan nama yang sama akan melempar PanelSchemaException::duplicateExportColumns(): file akan memiliki dua heading identik, sementara column picker menggunakan nama sebagai key pilihannya, sehingga mencentang satu akan mencentang keduanya.
Jika suatu nilai tidak boleh keluar dari aplikasi, jangan deklarasikan kolom tersebut. Tidak ada flag "hidden", dan memang tidak diperlukan — daftar kolom milik exporter adalah keseluruhan surface data yang boleh diexport.
query()
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
/**
* @param Builder<covariant Model> $query
* @return Builder<covariant Model>
*/
public static function query(Builder $query): Builder
{
// Eager loads for relation columns, and a stable order.
return $query->with('company')->reorder('id');
}2
3
4
5
6
7
8
9
10
11
12
Builder yang diterima sudah merepresentasikan seluruh record yang dicakup export — resource scope ditambah filter dan search tabel untuk ExportAction::make(), atau kumpulan record yang dipilih untuk ExportAction::bulk(). Ada dua hal yang tepat ditempatkan di sini:
- Eager load. Export sepuluh ribu row dengan relation column akan menjadi sepuluh ribu query tanpa eager loading.
- Urutan yang deterministik.
reorder('id')membuat dua export atas record yang sama dapat dibandingkan baris demi baris; tanpa itu, urutannya mengikuti sorting tabel ketika tombol ditekan.
Ini bukan tempat untuk menambahkan constraint yang mempersempit record set — table filter bertanggung jawab atas hal tersebut, dan constraint di sini juga akan berlaku pada bulk export sehingga dapat diam-diam membuang record yang secara eksplisit dicentang pengguna.
fileName()
Tanpa extension; ExportRun akan menambahkan extension berdasarkan format.
public static function fileName(): string
{
return 'users-'.date('Y-m-d');
}2
3
4
Default-nya adalah basename class dalam kebab-case ditambah Y-m-d-His — misalnya user-exporter-2026-08-15-114233 — karena setelah mengetahui isi file export, pertanyaan berikutnya biasanya adalah kapan file tersebut dibuat. Nama tanpa timestamp berarti export berikutnya menimpa export sebelumnya; itu masuk akal untuk "daftar terkini", tetapi buruk untuk audit trail.
disk() dan directory()
public static function disk(): string
{
return 'reports'; // a disk in config/filesystems.php
}
public static function directory(): string
{
return 'exports/users';
}2
3
4
5
6
7
8
9
File ditulis ke {directory}/{ownerKey}/{fileName}.{ext}, di mana owner adalah key pengguna yang meminta export. Download endpoint membangun segmen owner tersebut dari pengguna yang sedang meminta file, bukan dari request, sehingga seorang pengguna tidak dapat menebak atau menamai export milik pengguna lain melalui path.
Default menggunakan local, bukan public, dengan sengaja: export adalah salinan dari record yang sebelumnya hanya boleh dilihat oleh pengguna tertentu, dan public disk akan membuatnya tersedia melalui URL yang dapat ditebak. Jika memakai disk lain, jaga tetap private dan jangan letakkan di web root.
formats()
use PandaPanel\Actions\Enums\SpreadsheetFormat;
public static function formats(): array
{
return [SpreadsheetFormat::Csv];
}2
3
4
5
6
Secara default CSV dan XLSX sama-sama ditawarkan. Entry pertama menjadi pilihan default pada dialog, sehingga urutan list menentukan format yang paling sering diterima pengguna. Exporter yang hanya menawarkan satu format tetap menampilkan radio; field tersebut required dan hanya memiliki satu option.
escapesFormulas()
public static function escapesFormulas(): bool
{
return false; // only for a file another program parses
}2
3
4
Saat aktif, cell CSV yang diawali =, +, -, @, tab, atau carriage return akan diberi apostrophe di depan agar spreadsheet menampilkannya sebagai teks dan tidak menjalankannya sebagai formula. Ini adalah CWE-1236: penyerangnya adalah siapa pun yang dapat menulis ke text field, sedangkan korbannya adalah administrator yang membuka export tersebut.
Nonaktifkan hanya untuk file yang dibaca oleh program lain, ketika tidak ada apa pun yang mengevaluasi formula dan apostrophe tambahan justru menjadi korupsi data. Pengaturan ini tidak berpengaruh pada XLSX karena XLSX tidak membutuhkan mekanisme tersebut — lihat CSV dan XLSX.
chunkSize()
public static function chunkSize(): int
{
return 1000;
}2
3
4
Menentukan jumlah record yang ditahan di memory pada satu waktu. Row dihasilkan oleh generator melalui ->lazy(chunkSize()), sehingga export seratus ribu record hanya menahan sebanyak ukuran chunk dalam memory. Naikkan untuk menukar memory dengan lebih sedikit round trip; turunkan untuk model dengan kolom besar.
queueAfter()
public static function queueAfter(): int
{
return 5000;
}2
3
4
Jika jumlah record melebihi nilai ini, export didispatch ke PandaPanel\Jobs\RunPanelExport alih-alih dijalankan di dalam request. Nilainya berupa angka, bukan flag, karena kedua ekstrem sama-sama buruk: export kecil di background job memberikan pengalaman lebih lambat tanpa manfaat berarti, sedangkan export besar di request berisiko timeout.
| Nilai | Behavior |
|---|---|
0 | selalu masuk queue |
2000 | default — masuk queue jika lebih dari 2000 record |
| angka negatif apa pun | tidak pernah masuk queue |
completedMessage()
public static function completedMessage(int $records): string
{
return $records === 1
? 'Your export of 1 record is ready.'
: sprintf('Your export of %s records is ready.', number_format($records));
}2
3
4
5
6
Menjadi title notification sekaligus teks toast, baik pada jalur inline maupun queued.
Exporter lengkap
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Resources\Orders\Exports;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use PandaPanel\Actions\Enums\SpreadsheetFormat;
use PandaPanel\Actions\Exports\ExportColumn;
use PandaPanel\Actions\Exports\Exporter;
final class OrderExporter extends Exporter
{
/**
* @return list<ExportColumn>
*/
public static function columns(): array
{
return [
ExportColumn::make('reference')->label('Order'),
ExportColumn::make('customer.name')->label('Customer'),
ExportColumn::make('total')
->formatUsing(static fn (mixed $value): string => number_format((float) $value, 2)),
ExportColumn::make('status'),
ExportColumn::make('placed_at')->label('Placed'),
ExportColumn::make('internal_note')->enabledByDefault(false),
];
}
/**
* @param Builder<covariant Model> $query
* @return Builder<covariant Model>
*/
public static function query(Builder $query): Builder
{
return $query->with('customer')->reorder('id');
}
public static function fileName(): string
{
return 'orders-'.date('Y-m-d');
}
public static function formats(): array
{
return [SpreadsheetFormat::Xlsx, SpreadsheetFormat::Csv];
}
public static function chunkSize(): int
{
return 1000;
}
public static function queueAfter(): int
{
return 5000;
}
public static function completedMessage(int $records): string
{
return sprintf('%s orders exported.', number_format($records));
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
Menjalankan exporter tanpa action
PandaPanel\Actions\Exports\ExportRun::write() adalah writer lengkapnya dan bersifat public. Console command atau scheduled report dapat memanggilnya langsung:
use App\Models\Order;
use App\Panels\Admin\Resources\Orders\Exports\OrderExporter;
use PandaPanel\Actions\Enums\SpreadsheetFormat;
use PandaPanel\Actions\Exports\ExportRun;
$result = ExportRun::write(
OrderExporter::class,
Order::query()->where('status', 'shipped'),
['reference', 'total'], // the chosen names; [] means every column
SpreadsheetFormat::Xlsx,
$user->getKey(), // the owner directory the file is filed under
);
// ['path' => 'panel-exports/7/orders-2026-08-15.xlsx', 'file' => 'orders-2026-08-15.xlsx', 'records' => 412]2
3
4
5
6
7
8
9
10
11
12
13
14
Kode yang sama dijalankan di dalam request maupun queued job. Jika queued export menghasilkan file yang berbeda dari immediate export, itu berarti ada bug yang baru akan terlihat ketika jumlah row melewati threshold.
Dua hal selalu dijamin oleh writer apa pun input yang diberikan:
- Kolom ditulis mengikuti urutan deklarasi pada
columns(), bukan urutan nama yang diberikan ke pemanggilanwrite(). File dengan urutan kolom yang berubah-ubah akan sulit dibandingkan dengan export sebelumnya. columns: []berarti tulis semua kolom, bukan menghasilkan file tanpa kolom.
Catatan
- Semuanya static dan class tidak pernah diinstansiasi.
Exportertidak memiliki constructor, property instance, atau$this. Konfigurasi yang biasanya berada pada instance ditempatkan pada static method. - File dirakit terlebih dahulu di local disk.
ExportRunmenulis ketempnam(sys_get_temp_dir(), 'panel-export-'), kemudian men-stream file ke disk tujuan dan menghapus file temporary. Kedua writer membutuhkan path fisik: CSV men-stream ke handle dan XLSX menggunakanZipArchiveyang membuka file berdasarkan nama. - Exporter bukan table.
ExportColumnsengaja bukan table column: table column memahami sorting, searching, dan rendering HTML, sedangkan informasi seperti warna badge tidak memiliki tempat di spreadsheet. - Tidak ada proses otomatis untuk menghapus file lama. Lihat Storage dan cleanup.