Skip to content

Commit 9953e12

Browse files
authored
Merge pull request #63 from ingenerator/support-csv-unquoted-column-headers
Support CSV column headers written unquoted
2 parents 5d21d8a + 2998247 commit 9953e12

File tree

4 files changed

+89
-9
lines changed

4 files changed

+89
-9
lines changed

.github/workflows/test.yml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@ on:
55
# Only mainline branches, features etc are covered on the pull_request trigger
66
- '*.x'
77
pull_request:
8-
schedule:
9-
# Build every Monday morning in case of external issues
10-
- cron: '0 8 * * 1'
118

129
jobs:
1310
run-tests:
@@ -34,14 +31,16 @@ jobs:
3431
tools: composer:v2
3532

3633
- name: Checkout
37-
uses: actions/checkout@v2
34+
uses: actions/checkout@v4
35+
with:
36+
show-progress: false
3837

3938
- name: Get Composer Cache Directory
4039
id: composer-cache
4140
run: |
4241
echo "::set-output name=dir::$(composer config cache-files-dir)"
4342
44-
- uses: actions/cache@v2
43+
- uses: actions/cache@v4
4544
with:
4645
path: ${{ steps.composer-cache.outputs.dir }}
4746
key: ${{ runner.os }}-composer-${{ matrix.dependencies }}-${{ hashFiles('**/composer.lock') }}

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 option to write CSV column headers unquoted
6+
37
### v2.2.0 (2024-11-14)
48

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

src/CSV/CSVWriter.php

Lines changed: 20 additions & 2 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
{
@@ -32,7 +33,8 @@ class CSVWriter
3233
* @var array
3334
*/
3435
protected $options = [
35-
'write_utf8_bom' => FALSE
36+
'write_utf8_bom' => FALSE,
37+
'quote_headers' => TRUE,
3638
];
3739

3840
/**
@@ -72,7 +74,7 @@ public function write(array $row)
7274
\fputs($this->resource, static::UTF8_BOM);
7375
}
7476
$this->expect_schema = $row_schema;
75-
\fputcsv($this->resource, $this->expect_schema);
77+
$this->writeHeaders($this->expect_schema);
7678
} elseif ($this->expect_schema !== $row_schema) {
7779
throw MismatchedSchemaException::forSchema($this->expect_schema, $row_schema);
7880
}
@@ -97,4 +99,20 @@ public function close()
9799

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

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)