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
- [PHP-fgetcsv]PHP Manual: fgetcsv.
- [RFC4180]Yakov Shafranovich, Common Format and MIME Type for Comma-Separated Values (CSV) Files, RFC 4180, 2005.