What you will build
CSV exporters often survive simple rows and fail when a field contains both a backslash and a quote. Give the writer and reader the same explicit dialect, then test fields after a round trip. This example includes a comma, quotes, an embedded newline, a backslash before a quote, an empty field and a leading-zero identifier.
This is also a maintenance change for existing applications: the official fputcsv manual documents that relying on its default escape parameter is deprecated from PHP 8.4. Pass an empty escape string explicitly in both directions. The deprecation itself was not executed on the installed PHP 8.3 runtime.
Prerequisites
A supported PHP 8.x CLI. This example uses the PHP 8.1+ fputcsv end-of-line argument and was executed on PHP 8.3.6. No database or filesystem output is needed. Save as csv.php.
<?php
declare(strict_types=1);
$rows = [
['id', 'note'],
['001', 'comma, and "quote"'],
['002', "line one\nline two"],
['003', 'backslash before quote: \\"'],
['004', ''],
];
$stream = fopen('php://temp', 'w+');
if ($stream === false) throw new RuntimeException('open failed');
try {
foreach ($rows as $row) {
if (fputcsv($stream, $row, ',', '"', '', "\r\n") === false) {
throw new RuntimeException('write failed');
}
}
rewind($stream);
$decoded = [];
while (($row = fgetcsv($stream, 0, ',', '"', '')) !== false) {
$decoded[] = $row;
}
if ($decoded !== $rows) throw new RuntimeException('round trip mismatch');
echo 'round trip: ', count($decoded), " rows; strings preserved\n";
} finally {
fclose($stream);
}
Run and expected output
php -d error_reporting=E_ALL csv.phpround trip: 5 rows; strings preservedHow it works
The dialect uses a comma separator, double-quote enclosure, no proprietary escape character and CRLF between records. Disabling the extra escape mechanism leaves enclosure doubling to the CSV functions. It does not mean fields stop being quoted. The reader receives the same separator, enclosure and empty escape argument.
The stream is opened for reading and writing. Each write is checked for false, then rewind moves the cursor back to the beginning. The read loop checks strictly against false so a returned row is not mistaken for end-of-file. An embedded newline remains inside one field because fgetcsv performs CSV parsing; splitting on newline characters would lose that distinction.
Strict array comparison verifies values and types. The string 001 must remain a string with its leading zeroes. A mismatch throws an exception regardless of PHP assertion settings. Finally closes the stream even when a fixture fails. The expected count is five logical rows, including the header, rather than the number of physical lines.
Edge cases and scope
The contract uses strings in every field. CSV cannot itself preserve distinctions such as null versus an empty string, numeric types or application-specific dates. Add a schema at the import boundary if those distinctions matter. Coordinate dialect changes between producers and consumers rather than changing one side silently.
This verifies data preservation, not spreadsheet safety. A cell beginning with an equals sign can be interpreted as a formula by a spreadsheet. For spreadsheet-facing exports, define and test a separate display policy rather than claiming CSV quoting prevents formula evaluation. No spreadsheet application was used here.
Troubleshooting
If backslashes or quotes change, inspect the escape argument on every CSV read and write path, including SplFileObject wrappers. If multiline fields produce extra records, look for manual line splitting. If identifiers lose zeroes only after opening a spreadsheet, inspect its type inference; this round trip verifies strings before an external consumer is involved.
Primary references
Continue with PHP tutorials and all Java Samples categories.