Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions src/CsvWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,28 @@ 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);

$this->delimiter = $delimiter;
$this->enclosure = $enclosure;
$this->utf8Encoding = $utf8Encoding;
$this->prependHeaderRow = $prependHeaderRow;
$this->escape = $escape;
}

/**
Expand All @@ -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);
}
}
75 changes: 75 additions & 0 deletions tests/CsvWriterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}