PHP CSV Files
What Is a CSV File?
In simple terms: A CSV (Comma-Separated Values) file stores a table as plain text: one row per line, with values separated by commas. Spreadsheet programs such as Excel, Google Sheets, and LibreOffice can open and save CSV files, which makes CSV the most common format for importing and exporting data.
id,name,price
1,Gel Pen,20
2,Notebook,60
Technically: CSV is described in RFC 4180. Values containing the separator, double quotes, or line breaks are wrapped in double quotes, and double quotes inside values are doubled (""). PHP's fgetcsv(), fputcsv(), and str_getcsv() implement these rules, so you should never split or build CSV lines manually with explode() and implode().
Why Not Just Use explode(',')?
<?php
$line = '1,"Desk Lamp, LED",1299';
print_r(explode(',', $line));
print_r(str_getcsv($line, escape: ''));
Output
Array
(
[0] => 1
[1] => "Desk Lamp
[2] => LED"
[3] => 1299
)
Array
(
[0] => 1
[1] => Desk Lamp, LED
[2] => 1299
)
explode() breaks the quoted product name into two broken fields. str_getcsv() understands the quotes and returns the correct three values.
The escape Parameter
PHP's CSV functions historically used a non-standard backslash escape character (0), which can corrupt data containing backslashes. Since **PHP 8.4**, relying on the default value of the $escapeparameter is **deprecated**. Passescape: ''` (an empty string) to get standard RFC 4180 behavior — all examples in this lesson do so.
Writing CSV: fputcsv()
Syntax
fputcsv($stream, array $fields, string $separator = ",", string $enclosure = "\"", string $escape = "\\", string $eol = "\n"): int|false
Example
<?php
$products = [
['id', 'name', 'price'],
[1, 'Desk Lamp, LED', 1299],
[2, 'Notebook "A5"', 60],
[3, 'Gel Pen', 20],
];
$file = __DIR__ . '/products.csv';
$handle = fopen($file, 'w');
foreach ($products as $row) {
fputcsv($handle, $row, escape: '');
}
fclose($handle);
echo file_get_contents($file);
Output
id,name,price
1,"Desk Lamp, LED",1299
2,"Notebook ""A5""",60
3,Gel Pen,20
What fputcsv() Did
Desk Lamp, LEDcontains a comma, so it is wrapped in quotes.Notebook "A5"contains quotes, so it is wrapped in quotes and each inner quote is doubled.- Simple values are written without quotes.
fputcsv() returns the length of the written line, or false on failure.
Reading CSV: fgetcsv()
Example
<?php
$handle = fopen(__DIR__ . '/products.csv', 'r');
$header = fgetcsv($handle, escape: '');
$total = 0;
while (($row = fgetcsv($handle, escape: '')) !== false) {
$product = array_combine($header, $row);
$total += (int) $product['price'];
echo "{$product['id']}: {$product['name']} — ₹{$product['price']}\n";
}
fclose($handle);
echo "Total: ₹$total\n";
Output
1: Desk Lamp, LED — ₹1299
2: Notebook "A5" — ₹60
3: Gel Pen — ₹20
Total: ₹1379
How It Works
- The first
fgetcsv()call reads the header row. - Each later call reads one row as an array of strings, decoding quotes correctly. At the end of the file, it returns
false. array_combine($header, $row)turns each row into an associative array keyed by column name, so the code reads$product['price']instead of$row[2].- All values are strings; convert them (
(int),filter_var()) before calculating.
Because fgetcsv() reads one row at a time, this works for files of any size. (A blank line is returned as [null]; array_combine() would throw a ValueError for it, so real imports should skip or report rows whose column count does not match the header.)
Other Separators
Many European spreadsheets export CSV with semicolons, and tab-separated files (TSV) are also common:
$row = fgetcsv($handle, separator: ';', escape: '');
$row = fgetcsv($handle, separator: "\t", escape: '');
Practical Example: Importing With Validation
Imported data must be validated like any other user input. Reporting errors with line numbers lets the user fix the file:
<?php
$csv = <<<CSV
name,email,score
Asha,asha@example.com,88
Bala,bala@,72
Charu,charu@example.com,abc
"Dev, R",dev@example.com,95
CSV;
file_put_contents(__DIR__ . '/students.csv', $csv . "\n");
function importStudents(string $path): array
{
$handle = fopen($path, 'r');
if ($handle === false) {
throw new RuntimeException("Cannot open $path");
}
$header = fgetcsv($handle, escape: '');
$valid = [];
$errors = [];
$line = 1;
while (($row = fgetcsv($handle, escape: '')) !== false) {
$line++;
if (count($row) !== count($header)) {
$errors[] = "Line $line: expected " . count($header) . " columns";
continue;
}
$record = array_combine($header, array_map('trim', $row));
if (filter_var($record['email'], FILTER_VALIDATE_EMAIL) === false) {
$errors[] = "Line $line: invalid email '{$record['email']}'";
continue;
}
$score = filter_var($record['score'], FILTER_VALIDATE_INT, ['options' => ['min_range' => 0, 'max_range' => 100]]);
if ($score === false) {
$errors[] = "Line $line: score must be 0–100";
continue;
}
$valid[] = ['name' => $record['name'], 'email' => $record['email'], 'score' => $score];
}
fclose($handle);
return [$valid, $errors];
}
[$students, $errors] = importStudents(__DIR__ . '/students.csv');
echo "Imported: " . implode(', ', array_column($students, 'name')) . "\n";
echo implode("\n", $errors) . "\n";
Output
Imported: Asha, Dev, R
Line 3: invalid email 'bala@'
Line 4: score must be 0–100
Step-by-Step Explanation
- The header is line 1, so the line counter starts at 1 and increases before each data row.
- Rows with the wrong number of columns are reported instead of crashing
array_combine(). - Each field is trimmed and validated; invalid rows are collected with their line number.
- Valid rows are converted to typed values (the score becomes an
int). "Dev, R"is read correctly as one name, thanks to the quotes.
A real import would then insert the valid rows into the database inside a transaction, and show the errors to the user.
Exporting CSV for Download
To let users download a CSV file directly, write to php://output with the right headers:
<?php
// export-orders.php
$orders = [
['id' => 101, 'customer' => 'Asha', 'total' => 1250],
['id' => 102, 'customer' => 'பாலா', 'total' => 640],
];
header('Content-Type: text/csv; charset=UTF-8');
header('Content-Disposition: attachment; filename="orders-2026-10-15.csv"');
$output = fopen('php://output', 'w');
fwrite($output, "\xEF\xBB\xBF"); // UTF-8 BOM so Excel shows Tamil text correctly
fputcsv($output, ['Order ID', 'Customer', 'Total'], escape: '');
foreach ($orders as $order) {
fputcsv($output, [$order['id'], $order['customer'], $order['total']], escape: '');
}
fclose($output);
exit;
How It Works
Content-Disposition: attachmentmakes the browser download the response as a file with the given name.php://outputis a stream that writes directly to the response, so rows are sent as they are generated — even for very large exports — without building the whole file in memory.- The UTF-8 byte order mark (
EF BB BF) helps Excel recognize UTF-8, so non-English text such as Tamil names is displayed correctly instead of as garbled characters.
Security: CSV (Formula) Injection
When exported data is opened in a spreadsheet, a cell starting with =, +, -, or @ may be interpreted as a formula. An attacker who enters a "name" like =HYPERLINK("http://evil.example","Click") can create a malicious link in an administrator's export. Neutralize such values when exporting user-supplied data:
<?php
function csvSafe(mixed $value): mixed
{
if (is_string($value) && $value !== '' && in_array($value[0], ['=', '+', '-', '@', "\t", "\r"], true)) {
return "'" . $value;
}
return $value;
}
$output = fopen('php://output', 'w');
fputcsv($output, array_map('csvSafe', ['=HYPERLINK("http://evil.example","Click")', 'Asha', 1250]), escape: '');
fclose($output);
Output
"'=HYPERLINK(""http://evil.example"",""Click"")",Asha,1250
The leading apostrophe makes spreadsheets treat the value as text. Apply this only to text that came from users; numbers (including negative ones) can be passed through unchanged, as here.
SplFileObject for CSV
<?php
$file = new SplFileObject(__DIR__ . '/products.csv');
$file->setFlags(SplFileObject::READ_CSV | SplFileObject::SKIP_EMPTY | SplFileObject::READ_AHEAD | SplFileObject::DROP_NEW_LINE);
$file->setCsvControl(',', '"', '');
foreach ($file as $index => $row) {
if ($index === 0) {
continue; // header
}
echo $row[1] . "\n";
}
Output
Desk Lamp, LED
Notebook "A5"
Gel Pen
READ_CSV makes the iterator return parsed rows; setCsvControl() sets the separator, enclosure, and (empty) escape character. The flags skip the empty line at the end of the file.
Common Mistakes
Splitting CSV With explode()
Effect: Quoted values containing commas are broken apart.
Fix: Use fgetcsv() or str_getcsv().
Building CSV Lines by Hand
Effect: Values with commas, quotes, or line breaks corrupt the file.
Fix: Use fputcsv().
Relying on the Default escape
Effect: Backslashes in data are mishandled; deprecation notices in PHP 8.4+.
Fix: Pass escape: ''.
Trusting Imported Data
Effect: Invalid or malicious values enter the database.
Fix: Validate every row and report errors with line numbers.
Loading Huge Files Into Memory
Effect: Memory exhaustion with file() or file_get_contents() + str_getcsv().
Fix: Read row by row with fgetcsv() and write exports to a stream.
Best Practices
- Use
fgetcsv()/fputcsv()withescape: ''for standard-compliant CSV. - Map rows to the header with
array_combine()after checking column counts. - Validate imported rows like form input, and report errors by line number.
- Stream large imports and exports instead of building arrays in memory.
- Add a UTF-8 BOM for Excel users, and neutralize formula characters in exported user data.
Official reference: https://www.php.net/manual/en/function.fgetcsv.php