csanquer/colibri-csv
Colibri CSV is a lightweight, fast PHP library for reading and writing CSV files. It offers a fluent object-oriented API, streamed iteration for large files, and configurable delimiters, enclosures, and encodings, working standalone or in any PHP project.
Install with Composer. The package is csanquer/colibri-csv and exposes a small Colibri\ namespace (CsvReader, CsvWriter, and a shared AbstractCsv base class):
composer require csanquer/colibri-csv
Where to look first:
README.md — the examples cover 90% of real usage.src/Colibri/CsvReader.php — iterator behavior and the setter surface.src/Colibri/AbstractCsv.php — stream, delimiter, enclosure, and encoding handling shared by both reader and writer.First use case — read a delimited export row by row:
use Colibri\CsvReader;
$reader = new CsvReader(storage_path('app/imports/customers.csv'));
$reader->setDelimiter(';')
->setEnclosure('"')
->setEncoding('ISO-8859-1'); // source encoding; converted internally
// $reader->setSkipHeader(true);
foreach ($reader as $row) {
// $row is a plain array of fields
[$id, $name, $email] = $row;
}
The reader is an iterator, not an array-returning loader, so it streams the file and gives you constant-ish memory for large imports. Write your first transform (read → map → write) and you will have exercised the whole package.
Streaming import into a Laravel command. Wrap the reader in an artisan command and flush in chunks. Because iteration is lazy, a 500k-row file does not have to fit in memory:
foreach ($reader as $i => $row) {
if ($i === 0) { continue; } // or use the header-skip setter
Customer::updateOrCreate(['id' => $row[0]], ['name' => $row[1]]);
if ($i % 1000 === 0) { /* gc / progress output */ }
}
Header-aware processing. The package returns indexed arrays, so map headers manually. Read the first row as the header, then iterate the rest:
$iterator = $reader->getIterator();
$headers = $iterator->current(); // or rewind + current
$iterator->next();
foreach ($iterator as $row) {
$record = array_combine($headers, $row);
// $record['email'], ...
}
This also lets you validate required columns before processing any data.
Chunked / paginated reads. Use the offset and limit setters (exposed on the reader) to process a slice at a time — useful for resumable queue jobs where each job handles N rows:
$reader->setOffset($job->offset())->setLimit(5000);
Persist the new offset after the job commits so a retry is idempotent.
Writer for exports. The mirror pattern: build a CsvWriter, configure delimiter/enclosure to match the consumer (Excel in some locales wants ;), and write rows:
use Colibri\CsvWriter;
$writer = new CsvWriter(storage_path('app/exports/report.csv'));
$writer->setDelimiter(';')->setEnclosure('"');
$writer->writeLine(['id', 'name', 'email']);
foreach ($rows as $row) {
$writer->writeLine([$row->id, $row->name, $row->email]);
}
For direct downloads, point the writer at php://output (via stream) inside a response callback instead of writing to disk — that avoids the temp file entirely.
Streams over paths. Both reader and writer accept resources. That makes php://temp, php://memory, and remote streams (S3 via a wrapped stream) usable for tests:
$fh = fopen('php://temp', 'r+');
fwrite($fh, "a,b\n1,2\n"); rewind($fh);
$reader = (new CsvReader())->setStream($fh);
This is the cleanest way to unit-test CSV logic without fixtures on disk.
Laravel integration tips. Keep the parsing in a dedicated service/importer class and inject the path or stream; that isolates the dependency and makes swapping to league/csv trivial if you ever need to. Run large imports through a queued job and emit progress events per chunk. Use Storage::path() / Storage::readStream() to feed the reader so it works across local and S3 disks.
league/csv (MIT). Evaluate the license before writing code around it.$escape argument of fgetcsv/fputcsv/str_getcsv; if the package passes those implicitly you will see deprecation notices. Consider E_DEPRECATED filtering or contributing a patch.\xEF\xBB\xBF from the first value before validating headers.iconv/mbstring. The source-encoding setter relies on them; malformed bytes can silently corrupt fields or throw. Verify the extension is installed and validate a sample before batch processing.setDelimiter("\t"); semicolon exports need ;. Wrong delimiter produces single-column rows silently — assert the column count on the first row.fgetcsv (what the reader wraps) returns null for trailing empty fields, so count($row) can vary row to row. Normalize with array_pad or validate before array_combine.[null] or empty arrays. Skip them explicitly or enable the package's blank-line option, but confirm its behavior with a fixture — don't assume."null"/empty strings deliberately.fputcsv-style. It quotes when necessary, which can surprise downstream consumers expecting everything quoted. If your target requires forced quoting, the writer's fixed behavior will fight you — post-process or build a custom wrapper.CsvReader inside your own importer, transforming rows in your layer) rather than subclassing internals. Keep parsing logic in one adapter class so the swap to another library is a one-file change.How can I help you explore Laravel packages today?