raw Software

Parse CSV with PHP

Robert Eisele

CSV looks simple until a quoted field contains its own separator, quotation marks, or line breaks. PHP already handles those cases in fgetcsv(). A custom character-by-character parser usually adds edge cases without adding useful control.

Parse a CSV String

str_getcsv() parses one CSV record, not an entire document. To parse a string containing several records, write it to an in-memory stream and let fgetcsv() determine where each record ends:

<?php

declare(strict_types=1);

function parseCsv(
    string $contents,
    string $separator = ';',
    string $enclosure = '"'
): array {
    $stream = fopen('php://temp', 'r+');
    if ($stream === false) {
        throw new RuntimeException('Unable to create temporary CSV stream.');
    }

    try {
      if (fwrite($stream, $contents) !== strlen($contents)) {
            throw new RuntimeException('Unable to write CSV data.');
        }
        rewind($stream);

        $rows = [];
        while (($row = fgetcsv(
            stream: $stream,
            length: null,
            separator: $separator,
            enclosure: $enclosure,
            escape: ''
        )) !== false) {
            $rows[] = $row;
        }

        return $rows;
    } finally {
        fclose($stream);
    }
}

This input contains a separator, a quotation mark, and a line break inside quoted fields:

$csv = <<<'CSV'
name;note
Ada;"Uses semicolons; safely"
Grace;"Said ""hello""
and continued"
CSV;

$rows = parseCsv($csv);

The result preserves the field contents:

[
    ['name', 'note'],
    ['Ada', 'Uses semicolons; safely'],
    ['Grace', "Said \"hello\"\nand continued"],
]

Why the Empty Escape Argument Matters

In standard CSV, a quotation mark inside an enclosed field is represented by two quotation marks. Passing escape: '' disables PHP's proprietary backslash escape mechanism and keeps parsing compatible with that convention. Since PHP 8.4, relying on the default escape argument is deprecated; a future PHP version will change that default.

The separator and enclosure must each be exactly one byte. For a comma-separated file, call parseCsv($contents, ','). A tab can be passed as "\t". Multi-character separators are not CSV controls accepted by PHP's parser.

Stream a CSV File

When the data already lives in a file, do not load it into a string first. Reading one record at a time keeps memory use nearly constant, including when a quoted record spans physical lines:

<?php

$stream = fopen('import.csv', 'rb');
if ($stream === false) {
    throw new RuntimeException('Unable to open import.csv.');
}

try {
    while (($row = fgetcsv($stream, null, ',', '"', '')) !== false) {
        // Validate and process $row here.
    }
} finally {
    fclose($stream);
}

Avoid splitting the file with file(), explode(), or a line-oriented regular expression before parsing it. A physical line break may be part of a quoted field, so it is not necessarily a record boundary.

Headers and Validation

CSV parsing produces strings; it does not validate a schema. If the first record is a header, verify that names are present and unique before using them as array keys. For every subsequent record, compare its field count with the header count before calling array_combine(). This prevents malformed rows from being silently assigned to the wrong columns.

PHP returns a blank record as [null]. Decide explicitly whether blank records are meaningful or should be skipped. Do not call trim() on every field by default: spaces outside or inside a field may be intentional data. Convert numbers, dates, booleans, and null markers only after validating the corresponding column.

Encoding and UTF-8 BOMs

CSV does not declare its character encoding, and PHP's parser does not transcode input. Convert a known legacy encoding to UTF-8 before parsing. A UTF-8 byte-order mark is not removed automatically and otherwise becomes part of the first field. Strip it only at the beginning of the input:

if (str_starts_with($contents, "\xEF\xBB\xBF")) {
    $contents = substr($contents, 3);
}

For untrusted uploads, enforce file-size and record-count limits, reject unexpected column counts, and report row-level validation errors. Spreadsheet programs can interpret cells beginning with =, +, -, or @ as formulas when data is exported again; protect such values at the export boundary rather than changing them during parsing.

References