Skip to content

Commit 0fbb495

Browse files
committed
Support CSV column headers written unquoted
1 parent 5d21d8a commit 0fbb495

File tree

3 files changed

+86
-8
lines changed

3 files changed

+86
-8
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
### Unreleased
22

3+
### v2.3.0 (2025-03-10)
4+
5+
* Support CSV column headers being written unquoted with optional parameter to CSVWriter
6+
37
### v2.2.0 (2024-11-14)
48

59
* Add `->ago()` and `->future()` helper methods to RealtimeClock

src/CSV/CSVWriter.php

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99

1010
use Ingenerator\PHPUtils\CSV\MismatchedSchemaException;
11+
use RuntimeException;
1112

1213
class CSVWriter
1314
{
@@ -28,11 +29,9 @@ class CSVWriter
2829
*/
2930
protected $expect_schema;
3031

31-
/**
32-
* @var array
33-
*/
34-
protected $options = [
35-
'write_utf8_bom' => FALSE
32+
protected array $options = [
33+
'write_utf8_bom' => FALSE,
34+
'quote_headers' => TRUE,
3635
];
3736

3837
/**
@@ -72,7 +71,7 @@ public function write(array $row)
7271
\fputs($this->resource, static::UTF8_BOM);
7372
}
7473
$this->expect_schema = $row_schema;
75-
\fputcsv($this->resource, $this->expect_schema);
74+
$this->writeHeaders($this->expect_schema);
7675
} elseif ($this->expect_schema !== $row_schema) {
7776
throw MismatchedSchemaException::forSchema($this->expect_schema, $row_schema);
7877
}
@@ -97,4 +96,20 @@ public function close()
9796

9897
$this->resource = NULL;
9998
}
99+
100+
private function writeHeaders(array $keys): void
101+
{
102+
if ($this->options['quote_headers']) {
103+
\fputcsv($this->resource, $keys);
104+
} else {
105+
array_walk(
106+
$keys,
107+
fn(string $str) => ! str_contains($str, ',')
108+
?: throw new RuntimeException(
109+
"Column header `$str` cannot contain comma if headers are not quoted"
110+
)
111+
);
112+
fwrite($this->resource, implode(',', $keys).PHP_EOL);
113+
}
114+
}
100115
}

test/unit/CSV/CSVWriterTest.php

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
use Ingenerator\PHPUtils\CSV\CSVWriter;
1212
use Ingenerator\PHPUtils\CSV\MismatchedSchemaException;
1313
use LogicException;
14+
use PHPUnit\Framework\Attributes\DataProvider;
1415
use PHPUnit\Framework\Attributes\TestWith;
1516
use PHPUnit\Framework\TestCase;
17+
use RuntimeException;
1618
use function fclose;
1719
use function fgetcsv;
1820
use function file_get_contents;
@@ -134,6 +136,64 @@ public function test_it_does_not_write_column_headers_before_subsequent_rows()
134136
fclose($file);
135137
}
136138

139+
public static function providerQuotedHeaders(): array
140+
{
141+
$row = ['our data' => 'is at times', 'really big' => 'often'];
142+
143+
return [
144+
'default (with quotes)' => [
145+
$row,
146+
[],
147+
<<<CSV
148+
"our data","really big"
149+
"is at times",often
150+
151+
CSV,
152+
],
153+
'with quotes' => [
154+
$row,
155+
['quote_headers' => true],
156+
<<<CSV
157+
"our data","really big"
158+
"is at times",often
159+
160+
CSV,
161+
],
162+
'without quotes' => [
163+
$row,
164+
['quote_headers' => false],
165+
<<<CSV
166+
our data,really big
167+
"is at times",often
168+
169+
CSV,
170+
],
171+
];
172+
}
173+
174+
#[DataProvider('providerQuotedHeaders')]
175+
public function test_it_optionally_writes_column_headers_without_quotes(array $row, array $options, string $expect): void
176+
{
177+
$file = fopen('php://memory', 'w');
178+
$subj = $this->newSubject();
179+
$subj->open($file, $options);
180+
$subj->write($row);
181+
rewind($file);
182+
$this->assertSame($expect, stream_get_contents($file));
183+
fclose($file);
184+
}
185+
186+
public function test_it_throws_if_column_headers_contain_comma_when_unquoted(): void
187+
{
188+
$file = fopen('php://memory', 'w');
189+
$subj = $this->newSubject();
190+
$subj->open($file, ['quote_headers' => false]);
191+
$this->expectException(RuntimeException::class);
192+
$this->expectExceptionMessage('Column header `really, really big` cannot contain comma if headers are not quoted');
193+
$subj->write(['our data' => 'is, at times', 'really, really big' => 'often']);
194+
fclose($file);
195+
}
196+
137197
#[TestWith([true])]
138198
#[TestWith([false])]
139199
public function test_it_optionally_writes_byte_order_mark_at_start_of_file($write_bom)
@@ -189,8 +249,7 @@ protected function assertCSVContent(array $expect, $file)
189249
$this->assertSame($expect, $actual, 'CSV content should match');
190250
}
191251

192-
193-
protected function newSubject()
252+
private function newSubject(): CSVWriter
194253
{
195254
return new CSVWriter();
196255
}

0 commit comments

Comments
 (0)