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"