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
9 changes: 4 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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') }}
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
22 changes: 20 additions & 2 deletions src/CSV/CSVWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@


use Ingenerator\PHPUtils\CSV\MismatchedSchemaException;
use RuntimeException;

class CSVWriter
{
Expand All @@ -32,7 +33,8 @@ class CSVWriter
* @var array
*/
protected $options = [
'write_utf8_bom' => FALSE
'write_utf8_bom' => FALSE,
'quote_headers' => TRUE,
];

/**
Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
}
}
}
63 changes: 61 additions & 2 deletions test/unit/CSV/CSVWriterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
[],
<<<CSV
"our data","really big"
"is at times",often

CSV,
],
'with quotes' => [
$row,
['quote_headers' => true],
<<<CSV
"our data","really big"
"is at times",often

CSV,
],
'without quotes' => [
$row,
['quote_headers' => false],
<<<CSV
our data,really big
"is at times",often

CSV,
],
];
}

#[DataProvider('providerQuotedHeaders')]
public function test_it_optionally_writes_column_headers_without_quotes(array $row, array $options, string $expect): void
{
$file = fopen('php://memory', 'w');
$subj = $this->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)
Expand Down Expand Up @@ -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();
}
Expand Down
Loading