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; + } }