diff --git a/.editorconfig b/.editorconfig
index cac5bb7..9403682 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -11,3 +11,6 @@ indent_size = 4
[*.yml*]
indent_style = space
indent_size = 2
+
+[*.md]
+trim_trailing_whitespace = true
diff --git a/.gitignore b/.gitignore
index 9780341..5b8c118 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,7 @@
vendor/
-composer.phar
+build/
composer.lock
phpspec.yml
phpunit.xml
+.idea
+.phpunit.result.cache
diff --git a/.travis.yml b/.travis.yml
index d111709..f938b4d 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,22 +3,28 @@ dist: trusty
language: php
php:
- - 5.6
- - 7.1
- - 7.2
- 7.3
+ - 7.4
+ # - hhvm
+ # - nightly
matrix:
+ allow_failures:
+ # - php: hhvm
+ # - php: nightly
include:
- - php: 5.6
+ - php: 7.3
env:
- - COMPOSER_FLAGS="--prefer-lowest --prefer-stable"
+ - COMPOSER_FLAGS=" --prefer-stable"
- COVERAGE=true
- PHPUNIT_FLAGS="--coverage-clover=coverage.clover"
install:
- travis_retry composer update ${COMPOSER_FLAGS} --prefer-source --no-interaction
+before_script:
+ - export XDEBUG_MODE=coverage
+
script:
- vendor/bin/phpunit ${PHPUNIT_FLAGS}
diff --git a/composer.json b/composer.json
index 48d2ed7..31e39b9 100644
--- a/composer.json
+++ b/composer.json
@@ -23,17 +23,19 @@
"docs": "https://portphp.readthedocs.org"
},
"require": {
- "portphp/portphp": "^1.2.0"
+ "php": ">=7.3",
+ "portphp/portphp": "^2.0.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.2.0",
+ "phpspec/phpspec": "^7.0",
+ "friends-of-phpspec/phpspec-code-coverage": "^5.0"
},
"autoload": {
"psr-4": {
"Port\\Csv\\": "src/"
}
},
- "require-dev": {
- "phpunit/phpunit": "^4.0",
- "phpspec/phpspec": "^2.1"
- },
"autoload-dev": {
"psr-4": {
"Port\\Csv\\Tests\\": "tests/"
@@ -43,5 +45,11 @@
"branch-alias": {
"dev-master": "2.0.x-dev"
}
- }
+ },
+ "repositories": [
+ {
+ "type": "git",
+ "url": "https://github.com/klodoma/portphp-portphp"
+ }
+ ]
}
diff --git a/phpspec.yml.dist b/phpspec.yml.dist
index 0da615e..20fce67 100644
--- a/phpspec.yml.dist
+++ b/phpspec.yml.dist
@@ -2,4 +2,9 @@ suites:
library_suite:
namespace: Port\Csv
psr4_prefix: Port\Csv
+extensions:
+ FriendsOfPhpSpec\PhpSpec\CodeCoverage\CodeCoverageExtension: ~
formatter.name: pretty
+code_coverage:
+ format: clover
+ output: build/phpspec.coverage.xml
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 3d8fe50..4e12743 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -9,12 +9,23 @@
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
- syntaxCheck="false"
verbose="true"
- >
+>
./tests/
+
+
+
+ src
+
+
+
+
+
+
+
+
diff --git a/src/CsvReader.php b/src/CsvReader.php
index c50871f..79cdff5 100644
--- a/src/CsvReader.php
+++ b/src/CsvReader.php
@@ -4,13 +4,15 @@
use Port\Exception\DuplicateHeadersException;
use Port\Reader\CountableReader;
+use SeekableIterator;
+use SplFileObject;
/**
* Reads a CSV file, using as little memory as possible
*
* @author David de Boer
*/
-class CsvReader implements CountableReader, \SeekableIterator
+class CsvReader implements CountableReader, SeekableIterator
{
const DUPLICATE_HEADERS_INCREMENT = 1;
const DUPLICATE_HEADERS_MERGE = 2;
@@ -25,7 +27,7 @@ class CsvReader implements CountableReader, \SeekableIterator
/**
* CSV file
*
- * @var \SplFileObject
+ * @var SplFileObject
*/
protected $file;
@@ -74,21 +76,21 @@ class CsvReader implements CountableReader, \SeekableIterator
protected $duplicateHeadersFlag;
/**
- * @param \SplFileObject $file
+ * @param SplFileObject $file
* @param string $delimiter
* @param string $enclosure
* @param string $escape
*/
- public function __construct(\SplFileObject $file, $delimiter = ',', $enclosure = '"', $escape = '\\')
+ public function __construct(SplFileObject $file, $delimiter = ',', $enclosure = '"', $escape = '\\')
{
ini_set('auto_detect_line_endings', true);
$this->file = $file;
$this->file->setFlags(
- \SplFileObject::READ_CSV |
- \SplFileObject::SKIP_EMPTY |
- \SplFileObject::READ_AHEAD |
- \SplFileObject::DROP_NEW_LINE
+ SplFileObject::READ_CSV |
+ SplFileObject::SKIP_EMPTY |
+ SplFileObject::READ_AHEAD |
+ SplFileObject::DROP_NEW_LINE
);
$this->file->setCsvControl(
$delimiter,
@@ -108,12 +110,12 @@ public function current()
{
// If the CSV has no column headers just return the line
if (empty($this->columnHeaders)) {
- return $this->file->current();
+ return $this->getCurrentLine();
}
// Since the CSV has column headers use them to construct an associative array for the columns in this line
do {
- $line = $this->file->current();
+ $line = $this->getCurrentLine();
// In non-strict mode pad/slice the line to match the column headers
if (!$this->isStrict()) {
@@ -323,12 +325,13 @@ public function setStrict($strict)
protected function readHeaderRow($rowNumber)
{
$this->file->seek($rowNumber);
- $headers = $this->file->current();
+ $headers = $this->getCurrentLine();
// Test for duplicate column headers
$diff = array_diff_assoc($headers, array_unique($headers));
if (count($diff) > 0) {
switch ($this->duplicateHeadersFlag) {
+ /** @noinspection PhpMissingBreakStatementInspection */
case self::DUPLICATE_HEADERS_INCREMENT:
$headers = $this->incrementHeaders($headers);
// Fall through
@@ -404,4 +407,24 @@ protected function mergeDuplicates(array $line)
return $values;
}
+
+ /**
+ * Returns the current line from the file pointer.
+ * If found the BOM is removed
+ *
+ * @return array|false|string
+ */
+ protected function getCurrentLine()
+ {
+ $key = $this->file->key();
+ $line = $this->file->current();
+
+ //remove the BOM from the first line
+ if ($key === 0 && array_key_exists(0, $line)) {
+ $bom = pack('H*', 'EFBBBF');
+ $line[0] = preg_replace("/^$bom/", '', $line[0]);
+ }
+
+ return $line;
+ }
}
diff --git a/src/CsvReaderFactory.php b/src/CsvReaderFactory.php
index f985c30..18d8fab 100644
--- a/src/CsvReaderFactory.php
+++ b/src/CsvReaderFactory.php
@@ -3,6 +3,7 @@
namespace Port\Csv;
use Port\Reader\ReaderFactory;
+use SplFileObject;
/**
* Factory that creates CsvReaders
@@ -58,11 +59,11 @@ public function __construct(
}
/**
- * @param \SplFileObject $file
+ * @param SplFileObject $file
*
* @return CsvReader
*/
- public function getReader(\SplFileObject $file)
+ public function getReader(SplFileObject $file)
{
$reader = new CsvReader($file, $this->delimiter, $this->enclosure, $this->escape);
diff --git a/tests/CsvReaderFactoryTest.php b/tests/CsvReaderFactoryTest.php
index 15edd04..70afa83 100644
--- a/tests/CsvReaderFactoryTest.php
+++ b/tests/CsvReaderFactoryTest.php
@@ -1,21 +1,23 @@
getReader(new \SplFileObject(__DIR__.'/fixtures/data_column_headers.csv'));
+ $reader = $factory->getReader(new SplFileObject(__DIR__.'/fixtures/data_column_headers.csv'));
$this->assertInstanceOf('Port\Csv\CsvReader', $reader);
$this->assertCount(4, $reader);
$factory = new CsvReaderFactory(0);
- $reader = $factory->getReader(new \SplFileObject(__DIR__.'/fixtures/data_column_headers.csv'));
+ $reader = $factory->getReader(new SplFileObject(__DIR__.'/fixtures/data_column_headers.csv'));
$this->assertCount(3, $reader);
}
diff --git a/tests/CsvReaderTest.php b/tests/CsvReaderTest.php
index f60b000..b2132a0 100644
--- a/tests/CsvReaderTest.php
+++ b/tests/CsvReaderTest.php
@@ -1,14 +1,18 @@
setHeaderRowNumber(0);
@@ -37,7 +41,7 @@ public function testReadCsvFileWithColumnHeaders()
public function testReadCsvFileWithoutColumnHeaders()
{
- $file = new \SplFileObject(__DIR__.'/fixtures/data_no_column_headers.csv');
+ $file = new SplFileObject(__DIR__.'/fixtures/data_no_column_headers.csv');
$csvReader = new CsvReader($file);
$this->assertEmpty($csvReader->getColumnHeaders());
@@ -45,7 +49,7 @@ public function testReadCsvFileWithoutColumnHeaders()
public function testReadCsvFileWithManualColumnHeaders()
{
- $file = new \SplFileObject(__DIR__.'/fixtures/data_no_column_headers.csv');
+ $file = new SplFileObject(__DIR__.'/fixtures/data_no_column_headers.csv');
$csvReader = new CsvReader($file);
$csvReader->setColumnHeaders(array('id', 'number', 'description'));
@@ -58,7 +62,7 @@ public function testReadCsvFileWithManualColumnHeaders()
public function testReadCsvFileWithTrailingBlankLines()
{
- $file = new \SplFileObject(__DIR__.'/fixtures/data_blank_lines.csv');
+ $file = new SplFileObject(__DIR__.'/fixtures/data_blank_lines.csv');
$csvReader = new CsvReader($file);
$csvReader->setColumnHeaders(array('id', 'number', 'description'));
@@ -71,14 +75,14 @@ public function testReadCsvFileWithTrailingBlankLines()
public function testCountWithoutHeaders()
{
- $file = new \SplFileObject(__DIR__.'/fixtures/data_no_column_headers.csv');
+ $file = new SplFileObject(__DIR__.'/fixtures/data_no_column_headers.csv');
$csvReader = new CsvReader($file);
$this->assertEquals(3, $csvReader->count());
}
public function testCountWithHeaders()
{
- $file = new \SplFileObject(__DIR__.'/fixtures/data_column_headers.csv');
+ $file = new SplFileObject(__DIR__.'/fixtures/data_column_headers.csv');
$csvReader = new CsvReader($file);
$csvReader->setHeaderRowNumber(0);
$this->assertEquals(3, $csvReader->count(), 'Row count should not include header');
@@ -86,7 +90,7 @@ public function testCountWithHeaders()
public function testCountWithFewerElementsThanColumnHeadersNotStrict()
{
- $file = new \SplFileObject(__DIR__.'/fixtures/data_fewer_elements_than_column_headers.csv');
+ $file = new SplFileObject(__DIR__.'/fixtures/data_fewer_elements_than_column_headers.csv');
$csvReader = new CsvReader($file);
$csvReader->setStrict(false);
$csvReader->setHeaderRowNumber(0);
@@ -96,7 +100,7 @@ public function testCountWithFewerElementsThanColumnHeadersNotStrict()
public function testCountWithMoreElementsThanColumnHeadersNotStrict()
{
- $file = new \SplFileObject(__DIR__.'/fixtures/data_more_elements_than_column_headers.csv');
+ $file = new SplFileObject(__DIR__.'/fixtures/data_more_elements_than_column_headers.csv');
$csvReader = new CsvReader($file);
$csvReader->setStrict(false);
$csvReader->setHeaderRowNumber(0);
@@ -108,7 +112,7 @@ public function testCountWithMoreElementsThanColumnHeadersNotStrict()
public function testCountDoesNotMoveFilePointer()
{
- $file = new \SplFileObject(__DIR__.'/fixtures/data_column_headers.csv');
+ $file = new SplFileObject(__DIR__.'/fixtures/data_column_headers.csv');
$csvReader = new CsvReader($file);
$csvReader->setHeaderRowNumber(0);
@@ -121,7 +125,7 @@ public function testCountDoesNotMoveFilePointer()
public function testVaryingElementCountWithColumnHeadersNotStrict()
{
- $file = new \SplFileObject(__DIR__.'/fixtures/data_column_headers_varying_element_count.csv');
+ $file = new SplFileObject(__DIR__.'/fixtures/data_column_headers_varying_element_count.csv');
$csvReader = new CsvReader($file);
$csvReader->setStrict(false);
$csvReader->setHeaderRowNumber(0);
@@ -132,7 +136,7 @@ public function testVaryingElementCountWithColumnHeadersNotStrict()
public function testVaryingElementCountWithoutColumnHeadersNotStrict()
{
- $file = new \SplFileObject(__DIR__.'/fixtures/data_no_column_headers_varying_element_count.csv');
+ $file = new SplFileObject(__DIR__.'/fixtures/data_no_column_headers_varying_element_count.csv');
$csvReader = new CsvReader($file);
$csvReader->setStrict(false);
$csvReader->setColumnHeaders(array('id', 'number', 'description'));
@@ -143,7 +147,7 @@ public function testVaryingElementCountWithoutColumnHeadersNotStrict()
public function testInvalidCsv()
{
- $file = new \SplFileObject(__DIR__.'/fixtures/data_column_headers_varying_element_count.csv');
+ $file = new SplFileObject(__DIR__.'/fixtures/data_column_headers_varying_element_count.csv');
$reader = new CsvReader($file);
$reader->setHeaderRowNumber(0);
@@ -162,7 +166,7 @@ public function testInvalidCsv()
public function testLastRowInvalidCsv()
{
- $file = new \SplFileObject(__DIR__.'/fixtures/data_no_column_headers_varying_element_count.csv');
+ $file = new SplFileObject(__DIR__.'/fixtures/data_no_column_headers_varying_element_count.csv');
$reader = new CsvReader($file);
$reader->setColumnHeaders(array('id', 'number', 'description'));
@@ -188,11 +192,10 @@ public function testLineBreaks()
$this->assertCount(3, $reader);
}
- /**
- * @expectedException \Port\Exception\DuplicateHeadersException description
- */
public function testDuplicateHeadersThrowsException()
{
+ $this->expectException(DuplicateHeadersException::class);// description
+ $this->expectException(DuplicateHeadersException::class);
$reader = $this->getReader('data_column_headers_duplicates.csv');
$reader->setHeaderRowNumber(0);
}
@@ -223,6 +226,21 @@ public function testDuplicateHeadersIncrement()
);
}
+ public function testCSVWithBOM()
+ {
+ $reader = $this->getReader('data_with_bom.csv');
+ $reader->setHeaderRowNumber(0);
+ $this->assertSame(['id', 'number', 'description'], $reader->getColumnHeaders());
+ }
+
+ public function testCSVWithoutBOM()
+ {
+ $reader = $this->getReader('data_without_bom.csv');
+ $reader->setHeaderRowNumber(0);
+ $this->assertSame(['id', 'number', 'description'], $reader->getColumnHeaders());
+ }
+
+
public function testDuplicateHeadersMerge()
{
$reader = $this->getReader('data_column_headers_duplicates.csv');
@@ -254,7 +272,7 @@ public function testMaximumNesting()
ini_set('xdebug.max_nesting_level', 200);
- $file = new \SplTempFileObject();
+ $file = new SplTempFileObject();
for($i = 0; $i < 500; $i++) {
$file->fwrite("1,2,3\n");
}
@@ -274,7 +292,7 @@ public function testMaximumNesting()
protected function getReader($filename)
{
- $file = new \SplFileObject(__DIR__.'/fixtures/'.$filename);
+ $file = new SplFileObject(__DIR__.'/fixtures/'.$filename);
return new CsvReader($file);
}
diff --git a/tests/CsvWriterTest.php b/tests/CsvWriterTest.php
index 72e93bb..bdabadb 100644
--- a/tests/CsvWriterTest.php
+++ b/tests/CsvWriterTest.php
@@ -1,9 +1,8 @@
stream)) {
+ fclose($this->stream);
+ $this->stream = null;
+ }
+ }
+
+ protected function getStream()
+ {
+ if (!is_resource($this->stream)) {
+ $this->stream = fopen('php://temp', 'r+');
+ }
+
+ return $this->stream;
+ }
+
+ /**
+ * @param string $expected
+ * @param AbstractStreamWriter $actual
+ * @param string $message
+ */
+ public static function assertContentsEquals($expected, $actual, $message = '')
+ {
+ $stream = $actual->getStream();
+ rewind($stream);
+ $actual = stream_get_contents($stream);
+
+ self::assertEquals($expected, $actual, $message);
+ }
+}
diff --git a/tests/fixtures/data_with_bom.csv b/tests/fixtures/data_with_bom.csv
new file mode 100644
index 0000000..405095f
--- /dev/null
+++ b/tests/fixtures/data_with_bom.csv
@@ -0,0 +1,4 @@
+id,number,description
+50,123,"Description"
+6,456,"Another description"
+7,7890,"Some more info"
diff --git a/tests/fixtures/data_without_bom.csv b/tests/fixtures/data_without_bom.csv
new file mode 100644
index 0000000..5d10a38
--- /dev/null
+++ b/tests/fixtures/data_without_bom.csv
@@ -0,0 +1,4 @@
+id,number,description
+50,123,"Description"
+6,456,"Another description"
+7,7890,"Some more info"