A PHP export written with fputcsv() defaults breaks other CSV readers when a field contains a backslash; what is the escape-parameter trap and how do you fix it?
answer
- default escape is a backslash
- backslash-quote is not doubled on write
- reader keeps both characters
- field ending in a backslash swallows the next
- 8.4 deprecates relying on the default
basics
~20 sfputcsv(), fgetcsv() and str_getcsv() default escape to a backslash, a non-standard mechanism: a quote after a backslash is not doubled, and a trailing backslash can swallow the next field. Pass escape: '' on both sides.
solid answer
~40 sPHP's CSV functions have an `escape` parameter that defaults to `"\\"`. On write, `fputcsv()` doubles an enclosure quote *unless* it follows the escape character, so the field `a\"b` is written as `"a\"b"`, which a reader that only knows doubled quotes treats as broken. On read, `fgetcsv()` keeps both characters of a backslash-quote pair, so a field ending in a backslash, like `C:\temp\`, never closes and runs into the next field. Passing `escape: ''` (allowed since 7.4) disables the mechanism and leaves plain doubled-quote CSV. Since PHP 8.4, omitting `escape` raises `E_DEPRECATED`, and the manual says the default will change no earlier than 9.0, so pass it explicitly on every call.
code
php · 22 lines<?php
declare(strict_types=1);
$rows = [['id', 'path'], ['7', 'C:\\temp\\'], ['8', 'D:\\x']];
$h = fopen('php://temp', 'r+');
foreach ($rows as $r) {
fputcsv($h, $r, ',', '"', '\\'); // explicit old default
}
rewind($h);
while (($r = fgetcsv($h, null, ',', '"', '\\')) !== false) {
var_dump($r); // the id-7 record swallows the id-8 line
}
$h = fopen('php://temp', 'r+');
foreach ($rows as $r) {
fputcsv($h, $r, escape: '');
}
rewind($h);
while (($r = fgetcsv($h, escape: '')) !== false) {
var_dump($r); // three clean records
}go deeper
Remember that PHP's CSV functions have an escape argument defaulting to a backslash, and that passing an empty string turns it off.
Explain how the escape character stops quote doubling on write and why fgetcsv() keeps both characters on read.
Diagnose a broken export or a merged-column re-import from the escape default, fix call sites consistently, and plan re-exporting affected files.
Decide how a codebase adopts escape: '' across producers and consumers, including legacy files written with the old default and the announced 9.0 change.
## Two ways to put a quote inside a quoted field A CSV field that contains the separator, a quote or a line break is wrapped in an **enclosure** character, normally `"`. A quote inside such a field then needs protection. Standard CSV does it by **doubling**: `"say ""hi"""` means `say "hi"`. PHP's CSV functions support doubling, but they also have a second, **proprietary escape mechanism**, controlled by the `escape` parameter of `fputcsv()`, `fgetcsv()`, `str_getcsv()` and the matching `SplFileObject` methods. In PHP 8.5 the default is still `escape: "\\"` (a single backslash). The manual's own warning says any non-empty `escape` "can result in CSV that is not compliant with RFC 4180 or unable to survive a roundtrip through the PHP CSV functions". ## What the backslash does on write `fputcsv()` encloses a field when it contains the separator, the enclosure, the escape character, a line break, a tab or a space. While copying the field it doubles each enclosure character, **except one that directly follows the escape character**. So: | Field value | Written with default escape | Written with `escape: ''` | |---|---|---| | `say "hi"` | `"say ""hi"""` | `"say ""hi"""` | | `a\"b` | `"a\"b"` | `"a\""b"` | | `C:\temp\` | `"C:\temp\"` | `C:\temp\` | The second row is the trap for **other readers**: to a reader that only knows doubling, the quote after the backslash ends the field and the rest is stray text. ## What the backslash does on read `fgetcsv()` with the default escape treats a backslash followed by the enclosure as a literal pair and **keeps both characters** in the result. The manual's example: the line `"a""b","c\"d"` parses to `a"b` and `c\"d`. That has two consequences: - A field **ending** in a backslash, such as the Windows path `C:\temp\`, is written as `"C:\temp\"`. On reading it back, the final `\"` does not close the enclosure, so the parser carries on through the separator and merges following fields, or following lines, into one value. This is the round-trip failure inside PHP itself. - A file produced by another tool that happens to contain `\"` is parsed differently by PHP than by that tool. ## The fix 1. Pass `escape: ''` (an empty string, accepted since PHP 7.4) to **every** CSV call, reading and writing. That disables the proprietary mechanism; quotes are then only doubled. 2. Use named arguments to keep calls readable: `fputcsv($h, $fields, escape: '')`. 3. For `SplFileObject`, set it once with `setCsvControl(',', '"', '')`. 4. Re-export any stored files that were written with the default and contain backslashes before quotes or at field ends. ## The PHP 8.4 deprecation PHP 8.4 deprecated **relying on the default**: calling `fputcsv()`, `fgetcsv()` or `str_getcsv()` without an `escape` argument emits - `E_DEPRECATED`: "the $escape parameter must be provided as its default value will change". Passing `"\\"` explicitly silences it but keeps the old behaviour; passing `''` silences it and fixes the format. The manual states the default will change no earlier than PHP 9.0, so code that stays silent on the default today can change output on upgrade. Treat the deprecation as a prompt to decide, not a warning to suppress. ## Diagnosing it in production - Symptoms: a downstream importer reports "unexpected quote" or a wrong column count on a few rows; a PHP re-import merges two columns. - Grep the export for `\"` and for `\"` followed by the separator. - Check each call site for a missing or backslash `escape` argument; the 8.4 deprecation log lists the ones that omit it. ## Every place the escape argument appears | Call | Position of `escape` | |---|---| | `fgetcsv($stream, $length, $separator, $enclosure, $escape)` | 5th | | `fputcsv($stream, $fields, $separator, $enclosure, $escape, $eol)` | 5th | | `str_getcsv($string, $separator, $enclosure, $escape)` | 4th | | `SplFileObject::fgetcsv()` / `setCsvControl()` | 3rd | | `SplFileObject::fputcsv()` | 4th | A value longer than one byte throws a `ValueError` ("must be empty or a single character"). Because the position differs between functions, the named argument `escape: ''` is the least error-prone way to pass it.
- Does passing escape: "\\" explicitly make the code correct on PHP 8.4+?It silences the `E_DEPRECATED` notice but keeps the non-standard behaviour, so the trailing-backslash and backslash-quote problems remain. It is the right choice only when you must stay byte-compatible with files PHP already wrote. For new exports, pass `escape: ''`.
- Why is the problem often invisible in tests?Test fixtures rarely contain a backslash directly before a quote or at the end of a field. Most values round-trip identically with either setting, so only real data such as Windows paths, regex strings or escaped JSON in a text column triggers it. Add a fixture with `C:\temp\` and `a\"b`.
saying these in an interview costs you the question
- Believing the backslash escape is part of standard CSV
- Thinking passing "\\" explicitly fixes the output, not just the notice
- Assuming fgetcsv() strips the backslash from a backslash-quote pair
- Setting escape: '' only on the writer and not on the reader
- Suppressing the 8.4 deprecation instead of choosing an escape value