PHPReading & Writing CSV

Reading & Writing CSV

CSV (comma-separated values) is still one of the most common formats for exchanging tabular data — exports from spreadsheets, imports into accounting software, bulk data uploads. PHP's built-in fgetcsv() and fputcsv() handle the format's fiddly edge cases (commas and quotes inside fields, escaped quote characters) so you don't have to parse it with explode() and get it subtly wrong. This page covers reading with fgetcsv(), writing with fputcsv(), treating the first row as a header, the delimiter and enclosure parameters, and a full read-process-write example.

fgetcsv(): reading one row at a time

fgetcsv($handle) reads the next line from an open file handle and parses it into an array of fields, correctly handling values wrapped in quotes — including ones that contain commas or embedded newlines. It returns false once there are no more lines to read, which makes it a natural fit for a while loop.

fgetcsv-basic.php

PHP
<?php

$handle = fopen('contacts.csv', 'r');

if ($handle === false) {
    die('Could not open contacts.csv');
}

while (($row = fgetcsv($handle)) !== false) {
    print_r($row);
}

fclose($handle);
Array
(
    [0] => id
    [1] => name
    [2] => email
)
Array
(
    [0] => 1
    [1] => Priya Shah
    [2] => priya@example.com
)
Compare with !== false, not just a falsy check
Like several other file functions, `fgetcsv()` signals "no more data" with `false`. A data row that happens to be empty isn't the same as end-of-file, so the loop condition should explicitly compare against `false` rather than relying on a loose truthiness check.
Treating the first row as a header

CSV files usually use their first row as column names rather than data. Reading that row once with fgetcsv() before the main loop, and then combining it with each subsequent row via array_combine(), turns each row into an associative array keyed by column name — much easier to work with than numeric indexes you have to remember the meaning of.

csv-with-header.php

PHP
<?php

$handle = fopen('contacts.csv', 'r');
$header = fgetcsv($handle);

$rows = [];
while (($row = fgetcsv($handle)) !== false) {
    $rows[] = array_combine($header, $row);
}

fclose($handle);

print_r($rows[0]);
Array
(
    [id] => 1
    [name] => Priya Shah
    [email] => priya@example.com
)
fputcsv(): writing rows

fputcsv($handle, $fields) writes one array of values as a single, correctly quoted CSV line to an open handle — quoting any field that contains a comma, a quote character, or a newline, and escaping embedded quotes. Write the header row first, then loop over the data.

fputcsv-basic.php

PHP
<?php

$handle = fopen('export.csv', 'w');

fputcsv($handle, ['id', 'name', 'email']);
fputcsv($handle, [1, 'Priya Shah', 'priya@example.com']);
fputcsv($handle, [2, 'Tom "TJ" Lin', 'tom@example.com']);

fclose($handle);

echo file_get_contents('export.csv');
id,name,email
1,"Priya Shah",priya@example.com
2,"Tom ""TJ"" Lin",tom@example.com
Delimiter and enclosure parameters

Both functions accept optional parameters after the required ones: a delimiter character (a comma by default, but semicolons are common in some locales, and tabs are used for TSV-style files), and an enclosure character (a double quote by default) used to wrap fields that need it. Matching these to whatever produced the file — or whatever will consume the file you're writing — is essential; a file exported with semicolons but read with the default comma delimiter will parse every row as a single field.

delimiter-enclosure.php

PHP
<?php

// Reading a semicolon-delimited file
$handle = fopen('european-export.csv', 'r');
$row = fgetcsv($handle, 0, ';');
print_r($row);
fclose($handle);

// Writing with a single-quote enclosure instead of double quotes
$out = fopen('custom.csv', 'w');
fputcsv($out, ['value with, a comma', 'plain value'], ',', "'");
fclose($out);
Mismatched delimiters silently produce wrong data, not an error
Neither `fgetcsv()` nor `fputcsv()` warns you about a delimiter mismatch — a file read with the wrong delimiter just parses into one giant field per row instead of failing loudly. When importing a CSV from an unfamiliar source, open a few lines in a text editor first (or read the header row and check the field count) to confirm the delimiter before processing the whole file.
Full example: read, process, write

A common pattern is reading a CSV, transforming or filtering the data, and writing the result to a new file. The example below reads a list of orders, keeps only the ones over a minimum amount, adds a computed column, and writes the result to a new CSV.

process-orders.php

PHP
<?php

$in = fopen('orders.csv', 'r');
$out = fopen('large-orders.csv', 'w');

$header = fgetcsv($in);
$header[] = 'tax';
fputcsv($out, $header);

while (($row = fgetcsv($in)) !== false) {
    $data = array_combine(['id', 'customer', 'amount'], $row);

    if ((float) $data['amount'] < 100) {
        continue;
    }

    $tax = round((float) $data['amount'] * 0.08, 2);
    fputcsv($out, [$data['id'], $data['customer'], $data['amount'], $tax]);
}

fclose($in);
fclose($out);

echo 'Processed orders written to large-orders.csv';
Processed orders written to large-orders.csv
  • fgetcsv() reads and parses one row at a time from an open handle; it returns false at end of file.

  • Read the first row separately to use as a header, then array_combine() it with each data row.

  • fputcsv() writes one array as a correctly quoted CSV row.

  • The delimiter and enclosure parameters must match the file's actual format, or rows silently parse incorrectly.

  • Combining fgetcsv() and fputcsv() in one script is the standard way to filter or transform CSV data.

Tip
When a CSV's source is uncertain — a file uploaded by a user, for instance — validate the header row against the columns you expect before processing any data rows. Combined with treating the resulting file path as untrusted input (never build the save path directly from the uploaded filename), this catches malformed or unexpected files early instead of halfway through a long import.