From 55c71c4cf35439f51c867b566fe1ccff282374d4 Mon Sep 17 00:00:00 2001 From: Janez Urevc Date: Thu, 23 Jul 2026 10:33:33 +0200 Subject: [PATCH 1/2] Allow configuring escape for CsvWriter PHP 8.4 deprecates fputcsv() calls that omit the $escape argument. Add an optional constructor parameter (default '\\' for BC) and pass it through to fputcsv so callers can silence the deprecation and control escape behavior (including empty string for modern "no escape" CSV). Adds PHPUnit coverage that proves escape is applied and that writing works without deprecation exceptions under convertDeprecationsToExceptions. Supersedes #10. --- src/CsvWriter.php | 13 +++++-- tests/CsvWriterTest.php | 75 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/src/CsvWriter.php b/src/CsvWriter.php index fcdb8f5..1eb53a6 100644 --- a/src/CsvWriter.php +++ b/src/CsvWriter.php @@ -33,14 +33,20 @@ class CsvWriter extends AbstractStreamWriter */ protected $prependHeaderRow; + /** + * @var string + */ + private $escape; + /** * @param string $delimiter The delimiter * @param string $enclosure The enclosure * @param resource $stream * @param boolean $utf8Encoding * @param boolean $prependHeaderRow + * @param string $escape The escape character (pass '' for PHP 8.4+ preferred "no escape" behavior) */ - public function __construct($delimiter = ',', $enclosure = '"', $stream = null, $utf8Encoding = false, $prependHeaderRow = false) + public function __construct($delimiter = ',', $enclosure = '"', $stream = null, $utf8Encoding = false, $prependHeaderRow = false, string $escape = '\\') { parent::__construct($stream); @@ -48,6 +54,7 @@ public function __construct($delimiter = ',', $enclosure = '"', $stream = null, $this->enclosure = $enclosure; $this->utf8Encoding = $utf8Encoding; $this->prependHeaderRow = $prependHeaderRow; + $this->escape = $escape; } /** @@ -67,9 +74,9 @@ public function writeItem(array $item): void { if ($this->prependHeaderRow && 1 == $this->row++) { $headers = array_keys($item); - fputcsv($this->getStream(), $headers, $this->delimiter, $this->enclosure); + fputcsv($this->getStream(), $headers, $this->delimiter, $this->enclosure, $this->escape); } - fputcsv($this->getStream(), $item, $this->delimiter, $this->enclosure); + fputcsv($this->getStream(), $item, $this->delimiter, $this->enclosure, $this->escape); } } diff --git a/tests/CsvWriterTest.php b/tests/CsvWriterTest.php index 6556ba9..287109d 100644 --- a/tests/CsvWriterTest.php +++ b/tests/CsvWriterTest.php @@ -97,4 +97,79 @@ public function testHeaderPrependedWhenOptionSetToTrue() ); $writer->finish(); } + + /** + * Proves escape is applied: default '\\' and empty-string escape produce different CSV + * for a field that contains a backslash before a quote. + * + * Also exercises that fputcsv receives an explicit $escape argument, which is required + * on PHP 8.4+ (omitting it is deprecated; phpunit.xml converts deprecations to exceptions). + */ + public function testEscapeParameterAffectsOutput() + { + $item = array('a\\"b'); + + $defaultWriter = new CsvWriter(',', '"', fopen('php://temp', 'r+')); + $defaultWriter->setCloseStreamOnFinish(false); + $defaultWriter->prepare(); + $defaultWriter->writeItem($item); + $defaultOutput = $this->readWriterContents($defaultWriter); + + $emptyEscapeWriter = new CsvWriter(',', '"', fopen('php://temp', 'r+'), false, false, ''); + $emptyEscapeWriter->setCloseStreamOnFinish(false); + $emptyEscapeWriter->prepare(); + $emptyEscapeWriter->writeItem($item); + $emptyOutput = $this->readWriterContents($emptyEscapeWriter); + + $this->assertNotSame( + $defaultOutput, + $emptyOutput, + 'Custom escape should change CSV encoding of fields containing backslash/quote' + ); + + $this->assertSame($this->fputcsvString($item, '\\'), $defaultOutput); + $this->assertSame($this->fputcsvString($item, ''), $emptyOutput); + + fclose($defaultWriter->getStream()); + fclose($emptyEscapeWriter->getStream()); + } + + /** + * Empty escape is usable for modern "no escape" CSV and does not trigger PHP 8.4+ + * fputcsv deprecation (which phpunit.xml converts to exceptions). + */ + public function testEmptyEscapeWritesWithoutDeprecation() + { + $writer = new CsvWriter(',', '"', $this->getStream(), false, false, ''); + $writer->prepare(); + $writer->writeItem(array('hello', 'world')); + $writer->writeItem(array('say "hi"', 'path\\to')); + + $this->assertContentsEquals( + $this->fputcsvString(array('hello', 'world'), '') . + $this->fputcsvString(array('say "hi"', 'path\\to'), ''), + $writer + ); + + $writer->finish(); + } + + private function readWriterContents(CsvWriter $writer) + { + $stream = $writer->getStream(); + rewind($stream); + + return stream_get_contents($stream); + } + + private function fputcsvString(array $fields, $escape) + { + $stream = fopen('php://temp', 'r+'); + fputcsv($stream, $fields, ',', '"', $escape); + rewind($stream); + $contents = stream_get_contents($stream); + fclose($stream); + + return $contents; + } } From b2520ebbd37634f3d0f535cc545a2a453f17f3bb Mon Sep 17 00:00:00 2001 From: Janez Urevc Date: Thu, 23 Jul 2026 10:34:40 +0200 Subject: [PATCH 2/2] ci: re-trigger GitHub Actions