diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7bea078..3f0673d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,9 +5,6 @@ on: # Only mainline branches, features etc are covered on the pull_request trigger - '*.x' pull_request: - schedule: - # Build every Monday morning in case of external issues - - cron: '0 8 * * 1' jobs: run-tests: @@ -34,14 +31,16 @@ jobs: tools: composer:v2 - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 + with: + show-progress: false - name: Get Composer Cache Directory id: composer-cache run: | echo "::set-output name=dir::$(composer config cache-files-dir)" - - uses: actions/cache@v2 + - uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ matrix.dependencies }}-${{ hashFiles('**/composer.lock') }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d0f3d9..e889090 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ### Unreleased +### v2.3.0 (2025-03-10) + +* Support option to write CSV column headers unquoted + ### v2.2.0 (2024-11-14) * Add `->ago()` and `->future()` helper methods to RealtimeClock diff --git a/src/CSV/CSVWriter.php b/src/CSV/CSVWriter.php index f99ebc4..85282e8 100644 --- a/src/CSV/CSVWriter.php +++ b/src/CSV/CSVWriter.php @@ -8,6 +8,7 @@ use Ingenerator\PHPUtils\CSV\MismatchedSchemaException; +use RuntimeException; class CSVWriter { @@ -32,7 +33,8 @@ class CSVWriter * @var array */ protected $options = [ - 'write_utf8_bom' => FALSE + 'write_utf8_bom' => FALSE, + 'quote_headers' => TRUE, ]; /** @@ -72,7 +74,7 @@ public function write(array $row) \fputs($this->resource, static::UTF8_BOM); } $this->expect_schema = $row_schema; - \fputcsv($this->resource, $this->expect_schema); + $this->writeHeaders($this->expect_schema); } elseif ($this->expect_schema !== $row_schema) { throw MismatchedSchemaException::forSchema($this->expect_schema, $row_schema); } @@ -97,4 +99,20 @@ public function close() $this->resource = NULL; } + + private function writeHeaders(array $keys): void + { + if ($this->options['quote_headers']) { + \fputcsv($this->resource, $keys); + } else { + array_walk( + $keys, + fn(string $str) => ! str_contains($str, ',') + ?: throw new RuntimeException( + "Column header `$str` cannot contain comma if headers are not quoted" + ) + ); + fwrite($this->resource, implode(',', $keys).PHP_EOL); + } + } } diff --git a/test/unit/CSV/CSVWriterTest.php b/test/unit/CSV/CSVWriterTest.php index 2a0d489..76f0b3c 100644 --- a/test/unit/CSV/CSVWriterTest.php +++ b/test/unit/CSV/CSVWriterTest.php @@ -11,8 +11,10 @@ use Ingenerator\PHPUtils\CSV\CSVWriter; use Ingenerator\PHPUtils\CSV\MismatchedSchemaException; use LogicException; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; +use RuntimeException; use function fclose; use function fgetcsv; use function file_get_contents; @@ -134,6 +136,64 @@ public function test_it_does_not_write_column_headers_before_subsequent_rows() fclose($file); } + public static function providerQuotedHeaders(): array + { + $row = ['our data' => 'is at times', 'really big' => 'often']; + + return [ + 'default (with quotes)' => [ + $row, + [], + << [ + $row, + ['quote_headers' => true], + << [ + $row, + ['quote_headers' => false], + <<newSubject(); + $subj->open($file, $options); + $subj->write($row); + rewind($file); + $this->assertSame($expect, stream_get_contents($file)); + fclose($file); + } + + public function test_it_throws_if_column_headers_contain_comma_when_unquoted(): void + { + $file = fopen('php://memory', 'w'); + $subj = $this->newSubject(); + $subj->open($file, ['quote_headers' => false]); + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Column header `really, really big` cannot contain comma if headers are not quoted'); + $subj->write(['our data' => 'is, at times', 'really, really big' => 'often']); + fclose($file); + } + #[TestWith([true])] #[TestWith([false])] 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) $this->assertSame($expect, $actual, 'CSV content should match'); } - - protected function newSubject() + private function newSubject(): CSVWriter { return new CSVWriter(); }