Skip to content

Commit e3f743d

Browse files
authored
Allow configuring escape for CsvWriter (#17)
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.
1 parent e270cd7 commit e3f743d

2 files changed

Lines changed: 85 additions & 3 deletions

File tree

src/CsvWriter.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,21 +33,28 @@ class CsvWriter extends AbstractStreamWriter
3333
*/
3434
protected $prependHeaderRow;
3535

36+
/**
37+
* @var string
38+
*/
39+
private $escape;
40+
3641
/**
3742
* @param string $delimiter The delimiter
3843
* @param string $enclosure The enclosure
3944
* @param resource $stream
4045
* @param boolean $utf8Encoding
4146
* @param boolean $prependHeaderRow
47+
* @param string $escape The escape character (pass '' for PHP 8.4+ preferred "no escape" behavior)
4248
*/
43-
public function __construct($delimiter = ',', $enclosure = '"', $stream = null, $utf8Encoding = false, $prependHeaderRow = false)
49+
public function __construct($delimiter = ',', $enclosure = '"', $stream = null, $utf8Encoding = false, $prependHeaderRow = false, string $escape = '\\')
4450
{
4551
parent::__construct($stream);
4652

4753
$this->delimiter = $delimiter;
4854
$this->enclosure = $enclosure;
4955
$this->utf8Encoding = $utf8Encoding;
5056
$this->prependHeaderRow = $prependHeaderRow;
57+
$this->escape = $escape;
5158
}
5259

5360
/**
@@ -67,9 +74,9 @@ public function writeItem(array $item): void
6774
{
6875
if ($this->prependHeaderRow && 1 == $this->row++) {
6976
$headers = array_keys($item);
70-
fputcsv($this->getStream(), $headers, $this->delimiter, $this->enclosure);
77+
fputcsv($this->getStream(), $headers, $this->delimiter, $this->enclosure, $this->escape);
7178
}
7279

73-
fputcsv($this->getStream(), $item, $this->delimiter, $this->enclosure);
80+
fputcsv($this->getStream(), $item, $this->delimiter, $this->enclosure, $this->escape);
7481
}
7582
}

tests/CsvWriterTest.php

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,79 @@ public function testHeaderPrependedWhenOptionSetToTrue()
9797
);
9898
$writer->finish();
9999
}
100+
101+
/**
102+
* Proves escape is applied: default '\\' and empty-string escape produce different CSV
103+
* for a field that contains a backslash before a quote.
104+
*
105+
* Also exercises that fputcsv receives an explicit $escape argument, which is required
106+
* on PHP 8.4+ (omitting it is deprecated; phpunit.xml converts deprecations to exceptions).
107+
*/
108+
public function testEscapeParameterAffectsOutput()
109+
{
110+
$item = array('a\\"b');
111+
112+
$defaultWriter = new CsvWriter(',', '"', fopen('php://temp', 'r+'));
113+
$defaultWriter->setCloseStreamOnFinish(false);
114+
$defaultWriter->prepare();
115+
$defaultWriter->writeItem($item);
116+
$defaultOutput = $this->readWriterContents($defaultWriter);
117+
118+
$emptyEscapeWriter = new CsvWriter(',', '"', fopen('php://temp', 'r+'), false, false, '');
119+
$emptyEscapeWriter->setCloseStreamOnFinish(false);
120+
$emptyEscapeWriter->prepare();
121+
$emptyEscapeWriter->writeItem($item);
122+
$emptyOutput = $this->readWriterContents($emptyEscapeWriter);
123+
124+
$this->assertNotSame(
125+
$defaultOutput,
126+
$emptyOutput,
127+
'Custom escape should change CSV encoding of fields containing backslash/quote'
128+
);
129+
130+
$this->assertSame($this->fputcsvString($item, '\\'), $defaultOutput);
131+
$this->assertSame($this->fputcsvString($item, ''), $emptyOutput);
132+
133+
fclose($defaultWriter->getStream());
134+
fclose($emptyEscapeWriter->getStream());
135+
}
136+
137+
/**
138+
* Empty escape is usable for modern "no escape" CSV and does not trigger PHP 8.4+
139+
* fputcsv deprecation (which phpunit.xml converts to exceptions).
140+
*/
141+
public function testEmptyEscapeWritesWithoutDeprecation()
142+
{
143+
$writer = new CsvWriter(',', '"', $this->getStream(), false, false, '');
144+
$writer->prepare();
145+
$writer->writeItem(array('hello', 'world'));
146+
$writer->writeItem(array('say "hi"', 'path\\to'));
147+
148+
$this->assertContentsEquals(
149+
$this->fputcsvString(array('hello', 'world'), '') .
150+
$this->fputcsvString(array('say "hi"', 'path\\to'), ''),
151+
$writer
152+
);
153+
154+
$writer->finish();
155+
}
156+
157+
private function readWriterContents(CsvWriter $writer)
158+
{
159+
$stream = $writer->getStream();
160+
rewind($stream);
161+
162+
return stream_get_contents($stream);
163+
}
164+
165+
private function fputcsvString(array $fields, $escape)
166+
{
167+
$stream = fopen('php://temp', 'r+');
168+
fputcsv($stream, $fields, ',', '"', $escape);
169+
rewind($stream);
170+
$contents = stream_get_contents($stream);
171+
fclose($stream);
172+
173+
return $contents;
174+
}
100175
}

0 commit comments

Comments
 (0)