CSV
pop-csv moves data between a PHP array and CSV text. One object, Pop\Csv\Csv, holds both
representations — the array and the string — and converts in whichever direction you ask for. It handles
the file-versus-string ambiguity for you, so an import endpoint that might be handed either does not need
to work out which it has.
composer require popphp/pop-csv
Reading CSV#
The constructor takes CSV data and unserialize() turns it into an array of rows:
use Pop\Csv\Csv;
$csv = new Csv("first_name,last_name\nBob,Smith\nJane,Smith\n");
$rows = $csv->unserialize();
// [['first_name' => 'Bob', 'last_name' => 'Smith'], ['first_name' => 'Jane', 'last_name' => 'Smith']]
The header row becomes the array keys, and every value comes back as a string — CSV carries no types.
What the constructor argument means depends on its shape. A non-string is PHP data. A string is a file
path only when it contains .csv or .tsv and a file exists there, in which case it is read
immediately; any other string is raw CSV text, parsed later.
The static shortcuts skip the second call:
use Pop\Csv\Csv;
$csv = Csv::loadFile(__DIR__ . '/../var/import.csv'); // Csv object, already unserialized
$csv = Csv::loadString("a,b\n1,2\n"); // the same, from a string
$rows = Csv::getDataFromFile(__DIR__ . '/../var/import.csv'); // the array directly
All of them take the same options array the constructor does.
Csv::isValid() is a structural sanity check for a file you did not produce. It returns false for an
empty string, and for one whose data rows do not all have the first row's column count:
use Pop\Csv\Csv;
$string = "a,b\n1,2\n3,4\n";
if (Csv::isValid($string)) {
$rows = Csv::loadString($string)->getData();
}
It checks structure and nothing else — not encoding, not which delimiter is in use.
Everything above holds the whole result in memory. readRowsFromFile() returns a generator instead,
yielding one row at a time:
use Pop\Csv\Csv;
foreach (Csv::readRowsFromFile(__DIR__ . '/../var/large-import.csv') as $row) {
// one row at a time; memory stays flat however long the file is
}
Csv::getRowCountFromFile() streams the same way when all you want is a count — for a progress bar
before the import starts. Pass ['headers' => true] to subtract the header row; without it the header
counts as a row, so a four-record file reports 5.
A missing file throws Pop\Csv\Exception from both — "Error: The file '...' does not exist."
Writing CSV#
Going the other way, the constructor takes an array of rows and serialize() returns the CSV text. The
keys of the first row become the header:
use Pop\Csv\Csv;
$csv = new Csv([
['first_name' => 'Bob', 'last_name' => 'Smith'],
['first_name' => 'Jane', 'last_name' => 'Smith'],
]);
echo $csv->serialize();
first_name,last_name
Bob,Smith
Jane,Smith
writeToFile(), outputToHttp() and casting the object to a string all serialize first if that has not
happened yet, so echo $csv; produces the same text without the explicit call.
use Pop\Csv\Csv;
$rows = [
['first_name' => 'Bob', 'last_name' => 'Smith'],
['first_name' => 'Jane', 'last_name' => 'Smith'],
];
$csv = new Csv($rows);
$csv->writeToFile(__DIR__ . '/../var/export.csv');
$csv->outputToHttp('my-file.csv');
outputToHttp() sends Content-Disposition: attachment by default. Pass false as the second argument
for inline, and extra headers as the third. Csv::writeDataToFile() and Csv::outputDataToHttp() do the
same in one static call.
For a file too large to build in memory, append instead. The target has to exist already:
use Pop\Csv\Csv;
Csv::writeDataToFile([['first_name' => 'Bob', 'last_name' => 'Smith']], __DIR__ . '/../var/export.csv');
Csv::appendRowToFile(__DIR__ . '/../var/export.csv', ['first_name' => 'John', 'last_name' => 'Smith']);
Csv::appendDataToFile(__DIR__ . '/../var/export.csv', [['first_name' => 'Amy', 'last_name' => 'Jones']]);
Every append re-reads the target's header row and compares it against the new row's keys, throwing
Pop\Csv\Exception ("Error: The new data's columns do not match the CSV files columns.") on a mismatch.
The comparison includes order, so the right keys in the wrong order are rejected. Pass false as the last
argument to skip the check.
The Blank/Template methods write the header row alone, for a file someone fills in and sends back.
They derive it from the first data row's keys, so one representative row is still needed:
use Pop\Csv\Csv;
$rows = [['first_name' => 'Bob', 'last_name' => 'Smith']];
Csv::writeTemplateToFile($rows, __DIR__ . '/../var/template.csv'); // first_name,last_name
Csv::outputTemplateToHttp($rows, 'template.csv');
writeBlankFile() and outputBlankFileToHttp() are the instance equivalents, and both throw
Pop\Csv\Exception ("Error: The data has not been set.") when called before any data is on the object.
Options and Delimiters#
One options array covers both directions and is accepted by the constructor and by every static method.
| Option | Default | What it does |
|---|---|---|
delimiter |
, |
The field separator. "\t" gives TSV; any single character works |
enclosure |
" |
The quoting character |
escape |
" |
The character that escapes an enclosure inside a value |
fields |
true |
Write the header row. false writes data rows alone |
exclude |
[] |
Column names to leave out |
include |
[] |
Column names to keep, dropping everything else |
newline |
true |
Allow newlines inside a cell. false collapses them |
limit |
0 |
Truncate each cell to this many characters. 0 is no limit |
length |
0 |
Maximum line length read per row when parsing. 0 is no limit |
escapeFormulas |
false |
Prefix spreadsheet-formula cells with a single quote |
map |
[] |
Flatten a nested array to one of its keys |
columns |
[] |
Join a list of nested arrays on one of their keys |
Tab-separated data is the delimiter option and nothing else. A path ending in .tsv is auto-detected as
a file exactly as .csv is, but the delimiter is still passed on the way back in:
use Pop\Csv\Csv;
$rows = [['first_name' => 'Bob', 'last_name' => 'Smith']];
$csv = new Csv($rows, ['delimiter' => "\t"]);
$csv->writeToFile(__DIR__ . '/../var/export.tsv');
$loaded = Csv::loadFile(__DIR__ . '/../var/export.tsv', ['delimiter' => "\t"]);
escapeFormulas matters on any export carrying user-supplied text. A cell beginning =, +, - or @
is a live formula when the file opens in Excel, Google Sheets or LibreOffice; with the option on, such a
cell gets a leading single quote that the spreadsheet strips on display and never evaluates:
use Pop\Csv\Csv;
$csv = new Csv([['note' => '=SUM(A1:A10)']], ['escapeFormulas' => true]);
echo $csv->serialize(); // note\n'=SUM(A1:A10)\n
Pass escapeFormulas when the CSV is destined for a spreadsheet, so a cell beginning =, +, - or @
is written as text.
Working with Nested Data#
CSV is flat and the data you export usually is not. A nested array has to be reduced to one cell first,
and two options do that. map handles a single nested array: name the column and the key inside it.
columns handles a list of them: name the column and the key to pull from each entry, and the values are
joined into one cell.
use Pop\Csv\Csv;
$users = [
[
'id' => 1,
'username' => 'testuser',
'country' => ['name' => 'United States', 'code' => 'US'],
'roles' => [
['id' => 1, 'name' => 'Admin'],
['id' => 2, 'name' => 'Staff'],
],
],
];
$csv = new Csv($users, [
'map' => ['country' => 'code'],
'columns' => ['roles' => 'name'],
]);
echo $csv->serialize();
id,username,country,roles
1,testuser,US,"Admin,Staff"
country collapsed to US and roles to Admin,Staff — quoted, because the joined value contains the
delimiter.
Give both options a list of rows, even for a single record — the outer value is always the collection.
Both options are narrow on purpose — they cover a related record's label, not arbitrary nesting. Anything
else, flatten yourself before constructing the Csv.
Newlines inside a cell survive by default, quoted, which is valid CSV that some consumers handle badly.
['newline' => false] collapses each to a space, and ['limit' => n] truncates every cell to n
characters.
The Csv object's own accessors and the append variants' full parameter lists go past what this page
covers — see the pop-csv README.
See Also#
- File Storage — where an export written with
writeToFile()might land - Querying — the array of rows a query returns is what feeds
new Csv(...) - Requests & Responses — the response
outputToHttp()writes into - pop-csv README — every option and static shortcut