Make dealing with CSV data as easy and comfortable as possible.
Reads local files, URLs and Google Spreadsheets through one fluent API. Every
read is generator based, so a file of any size costs the same memory as a single
row — unless you explicitly ask for the whole thing with toArray().
PHP 8.3 or higher.
composer require heller/simple-csvuseHeller\SimpleCsv\Csv;
Csv::read('data.csv')->toArray();
Csv::read('https://example.com/data.csv')->toArray();
// Spreadsheet URLs are rewritten to their CSV export automatically
Csv::read('https://docs.google.com/spreadsheets/d/ABC123/edit')->toArray();An unreadable path throws a RuntimeException rather than returning an empty
result, so a typo in a filename cannot look like an empty import.
Csv::read('data.csv')->delimiter(';')->toArray();Fields are parsed the RFC 4180 way: a quote inside a quoted field is doubled,
and a backslash is just a character. Some producers, notably MySQL's
SELECT ... INTO OUTFILE, escape with a backslash instead:
Csv::read('dump.csv')->escape('\\')->toArray();Files that are not UTF-8 are converted while reading. Anything iconv knows
works as a name, Windows-1252 covers most Excel exports:
Csv::read('export.csv')->encoding('Windows-1252')->toArray();The conversion runs as a stream filter, so it costs nothing per row. Without
it, non-ASCII characters come back as invalid UTF-8 and anything downstream
that expects valid UTF-8 — toJson(), a database write — fails on them.
mapToHeaders() uses a row of the CSV as the keys for every data row. The
header row itself is never returned as data.
$rows = Csv::read('data.csv')->mapToHeaders()->toArray();
foreach ($rowsas$row) {
echo$row['columnname']; // instead of $row[3]
}Pass a row number if the header is not the first row:
Csv::read('data.csv')->mapToHeaders(3)->toArray();Pass an array to supply your own header names. No row is consumed, so every line in the file is treated as data:
Csv::read('data.csv')->mapToHeaders(['id', 'name', 'email'])->toArray();Read the header without reading the file:
Csv::read('data.csv')->getHeaderRow(); // ['Foo', 'Bar', 'Baz']getHeaderRow() returns the header as it appears in the file — skipColumns()
is not applied to it.
Rows with a different column count than the header keep their header keys.
Missing values become null, surplus values keep their column index:
// id,name,mail// 1,Ada// 2,Bob,b@x.de,extra
['id' => '1', 'name' => 'Ada', 'mail' => null]
['id' => '2', 'name' => 'Bob', 'mail' => 'b@x.de', 3 => 'extra']A UTF-8 BOM — written by Excel and Google Sheets — is stripped, so the first header name is usable as a key.
By default each row becomes a stdClass, so you can use property access:
Csv::read('data.csv')->mapToObject()->toArray();Pass a class name to map onto your own type. Values are assigned to properties whose names match the column, other columns are ignored:
Csv::read('data.csv')
->mapToObject(CsvRow::class)
->filter(fn (CsvRow$row) => $row->isValid())
->toArray();Column names are normalized to valid property names when mapping to objects: a
column Starts At (UTC) becomes $row->starts_at_utc. Note the difference to
mapToHeaders(), which keeps the original names as array keys.
mapToObject() implies mapToHeaders(), you do not need to call both.
Rows and columns are numbered from 1. Both methods take a single value or an
array, and skipColumns() also accepts column names.
Csv::read('data.csv')
->skipRows(1)
->skipColumns([2, 4, 'columnname'])
->toArray();skipRows() is independent of mapToHeaders() — the header row is skipped in
addition to whatever you list, in any call order.
Rows where every column is empty are returned by default. Drop them with:
Csv::read('data.csv')->skipEmptyRows()->toArray();The callback receives the row after mapping, so it gets an array or an object depending on what you configured. Filtering happens while reading, which keeps it cheap on large files.
Csv::read('data.csv')
->mapToHeaders()
->filter(fn ($row) => $row['column'] !== 'foo')
->toArray();$csv = Csv::read('data.csv')->mapToHeaders();
$csv->toArray(); // array of all rows$csv->toJson(); // JSON string of all rows$csv->first(); // first row, or null if there is none$csv->count(); // number of rows, with filter and skips appliedtoArray() and toJson() hold the entire file in memory. For anything large,
process row by row instead — this is memory constant and works on files with
millions of records:
Csv::read('data.csv')
->mapToObject(CsvRow::class)
->each(function (CsvRow$row) {
// import or handle the row however you like
});Csv::make($rows)->toFile('out.csv')->write();write() replaces the file. Pass header names to get them as the first row:
Csv::make([['Ada', 'Berlin']])
->withHeaders(['name', 'city'])
->toFile('out.csv')
->write();
// name,city// Ada,BerlinAssociative rows are put into header order regardless of the order of their keys, and a column a row does not carry is written empty:
Csv::make([['city' => 'Berlin', 'name' => 'Ada'], ['name' => 'Bob']])
->withHeaders(['name', 'city'])
->toFile('out.csv')
->write();
// name,city// Ada,Berlin// Bob,Objects are written by their public properties, so anything read with
mapToObject() can be written straight back out.
append() keeps the existing contents and does not repeat the header row. When
no headers are set, the header already in the file defines the column order:
Csv::make([['city' => 'Hamburg', 'name' => 'Bob']])
->toFile('out.csv')
->append();If the file is missing or empty, append() writes it like write() would,
header included.
insertAt() puts rows in front of an existing record. Records are counted from
1 and the header is record 1, so the first data row is position 2:
// Col1,Col2// A,A// B,B
Csv::make([['NEW', 'NEW']])->toFile('data.csv')->insertAt(3);
// Col1,Col2// A,A// NEW,NEW// B,BEverything after the insert is copied byte for byte, so quoting and spacing of untouched records survive. The file is rebuilt next to itself and moved into place in one step, which means a crash mid-write cannot leave a half-written file behind. Memory stays constant regardless of file size — inserting into a 1M row file costs about 2 MB.
A position past the end appends. A missing or empty file is written from
scratch, like write().
Csv::make($rows)->delimiter(';')->toFile('out.csv')->write();Rows end with \n. Excel on Windows expects \r\n, and needs a UTF-8 BOM to
read anything outside ASCII correctly:
Csv::make($rows)->delimiter(';')->crlf()->bom()->toFile('export.csv')->write();bom() only applies to write(). append() and insertAt() leave a file
that already has content alone.
- Output is always UTF-8. Only reading converts between encodings.
- Existing records can be inserted in front of, but not changed or removed.
composer test# pest
composer lint # pint
composer bench # 1M row benchmark, generates its own fixtureMIT. See LICENSE.