skip to content

In PHP, why does calling str_getcsv() on each piece of explode("\n", $csv) corrupt records whose quoted fields contain line breaks?

level: juniorimportance: should knowfreq 33%

answer

  1. one string in, one record out
  2. explode cannot see quotes
  3. the handle-based reader can continue a record
  4. php://temp turns a string into a stream

basics

~10 s

str_getcsv() parses one string as one record, and explode() splits on every newline, including those inside quoted fields. Feed the text to fgetcsv() through a php://temp stream instead, which continues a record across lines.

solid answer

~40 s

`str_getcsv($string)` parses its input as a **single record** and returns an array of fields. Splitting the document with `explode("\n", ...)` first cuts at every line break, including one inside a quoted field such as an address, so one record becomes two broken halves: the first has an unclosed quote, the second starts mid-field. Passing the whole document to `str_getcsv()` is wrong too, because it does not split rows, so unquoted line breaks end up inside field values. The fix is to parse with a stream-based reader: write the string into `fopen('php://temp', 'r+')`, `rewind()` it, and loop `fgetcsv()`, which reads more lines until an open enclosure closes. `str_getcsv()` fits text known to be exactly one record, like a single line of a header.

code

php · 22 lines
php
<?php
declare(strict_types=1);

$csv = "name,address\n\"Ana Silva\",\"12 Rua Nova\nLisbon\"\n";

// Wrong: 3 pieces, the address is cut in two
$broken = array_map(
    fn(string $line): array => str_getcsv($line, ',', '"', ''),
    array_filter(explode("\n", $csv), fn(string $l): bool => $l !== '')
);

// Right: 2 records, the address keeps its line break
$h = fopen('php://temp', 'r+');
fwrite($h, $csv);
rewind($h);
$rows = [];
while (($row = fgetcsv($h, null, ',', '"', '')) !== false) {
    $rows[] = $row;
}
fclose($h);

var_dump(count($broken), count($rows)); // int(3) int(2)

go deeper

for a junior

Recall that str_getcsv() parses one record from one string, while fgetcsv() reads the next record from an open handle.

for a middle

Explain how a quoted line break breaks line splitting, and how php://temp lets fgetcsv() parse CSV that arrived as a string.

for a senior

Spot silent corruption in imports: extra rows and truncated fields that raise no error, and add a fixture with an embedded line break to catch it.

for a principal

Set the team rule that CSV is always parsed through a stream reader, and decide where untrusted CSV text enters as a stream rather than a string.

## What each function parses PHP has three CSV readers, and they differ in **where they get their input**: | Function | Input | Output | |---|---|---| | `str_getcsv($string, ...)` | one string | one record: `array` of fields | | `fgetcsv($stream, ...)` | an open stream handle | the next record, or `false` at the end | | `SplFileObject::fgetcsv()` | the file object | the next record, or `false` | All three share the same parser and the same `separator`, `enclosure` and `escape` arguments. The difference is that the stream-based ones can **read more input**: when a quoted field is still open at the end of a line, `fgetcsv()` pulls the next line from the stream and continues the same field. `str_getcsv()` has nothing more to read, so an unclosed quote simply runs to the end of the string it was given. ## Why explode() plus str_getcsv() breaks Consider this document with a multi-line address: ``` name,address "Ana Silva","12 Rua Nova Lisbon" ``` `explode("\n", $csv)` returns three strings, not two records: 1. `name,address` parses fine. 2. `"Ana Silva","12 Rua Nova` has an unterminated enclosure; `str_getcsv()` returns `Ana Silva` and `12 Rua Nova` as the second field. 3. `Lisbon"` becomes a new record whose single field includes a stray quote. The import now has an extra row and a truncated address, and nothing raised an error. It also fails for Windows line endings split on `\n` alone, which leave a trailing `\r` in each piece. ## Why the whole document in str_getcsv() breaks too `str_getcsv()` does not treat line breaks as record boundaries. An unquoted field is read up to the next separator, so given `a,b\nc,d` it returns three fields with the middle one containing `b`, a newline and `c`. You get one malformed record instead of two. ## The fix: give the parser a stream When CSV arrives as a string (an API response body, a text column, a request payload already in memory), wrap it in a temporary stream: - `$h = fopen('php://temp', 'r+');` opens a read/write stream kept in memory and spilled to a temporary file when it grows; - `fwrite($h, $csv); rewind($h);` loads it and moves back to the start; - loop `while (($row = fgetcsv($h, null, ',', '"', '')) !== false)` to get correct records. For a file on disk, skip the string entirely and `fopen()` the file. ## When str_getcsv() is the right tool - The input is known to be one record: a single line from a line-oriented log, a comma-separated header value, one cell list typed into a form field. - You already have one complete record from another parser. In PHP 8.4+, `str_getcsv()` also throws a `ValueError` for an invalid `separator`, `enclosure` or `escape` (matching `fgetcsv()` and `fputcsv()`), and omitting `escape` raises the same `E_DEPRECATED` notice as the other CSV functions. ## Line endings `fgetcsv()` recognises both `\n` and `\r\n` line ends and does not leave the `\r` in the last field. Files that use a lone `\r` (old Mac style) are a different matter: the `auto_detect_line_endings` ini setting that used to help was deprecated in PHP 8.1, and the manual now says to handle `\r` line breaks manually. Splitting with `explode("\n", ...)` has neither advantage: it leaves `\r` on every piece of a Windows file. ## Spotting the bug in review - `explode("\n"` or `file(` immediately followed by `str_getcsv(` is the pattern; - the symptom is an import with more records than the source has rows, or fields that start or end with a stray quote; - a fixture with one quoted multi-line field catches it in a unit test.

  • What does str_getcsv() return for a string whose last quoted field is never closed?
    It returns the fields it could read, with everything from the opening quote to the end of the string as the last field; no error is raised. Since PHP 8.3 an unterminated enclosure that holds nothing yields an empty string rather than a string with a single null byte.

saying these in an interview costs you the question

  • Believing str_getcsv() splits a multi-line document into rows
  • Assuming explode() on newlines respects quoted fields
  • Expecting str_getcsv() to throw on an unclosed quote
  • Loading a file into a string just to call str_getcsv() on lines