In PHP, why does calling str_getcsv() on each piece of explode("\n", $csv) corrupt records whose quoted fields contain line breaks?
answer
- one string in, one record out
- explode cannot see quotes
- the handle-based reader can continue a record
- php://temp turns a string into a stream
basics
~10 sstr_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
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
Recall that str_getcsv() parses one record from one string, while fgetcsv() reads the next record from an open handle.
Explain how a quoted line break breaks line splitting, and how php://temp lets fgetcsv() parse CSV that arrived as a string.
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.
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