From 12d65a6d83cc2affc38a8c96b1e65d106febd90e Mon Sep 17 00:00:00 2001 From: chadicus Date: Sat, 9 Jun 2018 08:22:24 -0400 Subject: [PATCH 01/18] Add CsvOptions --- src/CsvOptions.php | 83 ++++++++++++++++++++++++++++++++++++++++ tests/CsvOptionsTest.php | 73 +++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 src/CsvOptions.php create mode 100644 tests/CsvOptionsTest.php diff --git a/src/CsvOptions.php b/src/CsvOptions.php new file mode 100644 index 0000000..0a621de --- /dev/null +++ b/src/CsvOptions.php @@ -0,0 +1,83 @@ +delimiter = $delimiter; + $this->enclosure = $enclosure; + $this->escapeChar = $escapeChar; + } + + /** + * Gets the field delimiter (one character only). + * + * @return string + */ + public function getDelimiter() : string + { + return $this->delimiter; + } + + /** + * Gets the field enclosure character (one character only). + * + * @return string + */ + public function getEnclosure() : string + { + return $this->enclosure; + } + + /** + * Gets the escape character (one character only). + * + * @return string + */ + public function getEscapeChar() : string + { + return $this->escapeChar; + } +} diff --git a/tests/CsvOptionsTest.php b/tests/CsvOptionsTest.php new file mode 100644 index 0000000..b9be611 --- /dev/null +++ b/tests/CsvOptionsTest.php @@ -0,0 +1,73 @@ +assertSame(',', (new CsvOptions(',', '"', '\\'))->getDelimiter()); + } + + /** + * @test + * @covers ::getEnclosure + */ + public function getEnclosure() + { + $this->assertSame('"', (new CsvOptions(',', '"', '\\'))->getEnclosure()); + } + + /** + * @test + * @covers ::getEscapeChar + */ + public function getEscapeChar() + { + $this->assertSame('\\', (new CsvOptions(',', '"', '\\'))->getEscapeChar()); + } +} From f3353681a7e78e919c138e645247401b92db04c5 Mon Sep 17 00:00:00 2001 From: chadicus Date: Sat, 9 Jun 2018 08:30:54 -0400 Subject: [PATCH 02/18] Use CsvOptions in Reader --- src/Reader.php | 64 +++++++++++++------------------------------- tests/ReaderTest.php | 50 +++------------------------------- 2 files changed, 21 insertions(+), 93 deletions(-) diff --git a/src/Reader.php b/src/Reader.php index 9c0ca70..951a916 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -14,27 +14,6 @@ class Reader implements \Iterator */ private $headers; - /** - * The field delimiter (one character only). - * - * @var string - */ - private $delimiter; - - /** - * The field enclosure character (one character only). - * - * @var string - */ - private $enclosure; - - /** - * The escape character (one character only). - * - * @var string - */ - private $escapeChar; - /** * File pointer to the csv file. * @@ -56,22 +35,22 @@ class Reader implements \Iterator */ private $current = null; + /** + * @var CsvOptions + */ + private $csvOptions; + /** * Create a new Reader instance. * - * @param string $file The full path to the csv file. - * @param array $headers The column headers. If null, the headers will be derived from the first line in the - * file. - * @param string $delimiter The field delimiter (one character only). - * @param string $enclosure The field enclosure character (one character only). - * @param string $escapeChar The escape character (one character only). + * @param string $file The full path to the csv file. + * @param array $headers The column headers. If null, the headers will be derived from the first line in + * the file. + * @param CsvOptions $csvOptions Options for the csv file. * * @throws \InvalidArgumentException Thrown if $file is not readable. - * @throws \InvalidArgumentException Thrown if $delimiter is a single character string. - * @throws \InvalidArgumentException Thrown if $enclosure is a single character string. - * @throws \InvalidArgumentException Thrown if $escapeChar is a single character string. */ - public function __construct($file, array $headers = null, $delimiter = ',', $enclosure = '"', $escapeChar = '\\') + public function __construct($file, array $headers = null, CsvOptions $csvOptions = null) { if (!is_readable((string)$file)) { throw new \InvalidArgumentException( @@ -79,22 +58,9 @@ public function __construct($file, array $headers = null, $delimiter = ',', $enc ); } - if (strlen($delimiter) !== 1) { - throw new \InvalidArgumentException('$delimiter must be a single character string'); - } - - if (strlen($enclosure) !== 1) { - throw new \InvalidArgumentException('$enclosure must be a single character string'); - } - - if (strlen($escapeChar) !== 1) { - throw new \InvalidArgumentException('$escapeChar must be a single character string'); - } + $this->csvOptions = $csvOptions ?? new CsvOptions(); $this->headers = $headers; - $this->delimiter = $delimiter; - $this->enclosure = $enclosure; - $this->escapeChar = $escapeChar; $this->handle = fopen((string)$file, 'r'); } @@ -140,7 +106,13 @@ public function next() */ private function readLine() { - $raw = fgetcsv($this->handle, 0, $this->delimiter, $this->enclosure, $this->escapeChar); + $raw = fgetcsv( + $this->handle, + 0, + $this->csvOptions->getDelimiter(), + $this->csvOptions->getEnclosure(), + $this->csvOptions->getEscapeChar() + ); if (empty($raw)) { throw new \Exception('Empty line read'); } diff --git a/tests/ReaderTest.php b/tests/ReaderTest.php index 6c9dcd8..ec94d51 100644 --- a/tests/ReaderTest.php +++ b/tests/ReaderTest.php @@ -1,6 +1,7 @@ Date: Sat, 9 Jun 2018 10:01:59 -0400 Subject: [PATCH 03/18] Add HeaderStrategy --- src/HeaderStrategy.php | 77 +++++++++++++++++++++++++++++++++ src/HeaderStrategyInterface.php | 10 +++++ tests/HeaderStrategyTest.php | 63 +++++++++++++++++++++++++++ 3 files changed, 150 insertions(+) create mode 100644 src/HeaderStrategy.php create mode 100644 src/HeaderStrategyInterface.php create mode 100644 tests/HeaderStrategyTest.php diff --git a/src/HeaderStrategy.php b/src/HeaderStrategy.php new file mode 100644 index 0000000..9a67804 --- /dev/null +++ b/src/HeaderStrategy.php @@ -0,0 +1,77 @@ +getHeadersCallable = $getHeadersCallable; + } + + /** + * Create header strategy which derives the headers from the first line of the file. + * + * @return HeaderstrategyInterface + */ + public static function derive() : HeaderStrategyInterface + { + return new self( + function (SplFileObject $fileObject) : array { + $row = $fileObject->fgetcsv(); + $fileObject->rewind(); + return $row; + } + ); + } + + /** + * Create header strategy which uses the provided headers array. + * + * @return HeaderstrategyInterface + */ + public static function provide(array $headers) : HeaderStrategyInterface + { + return new self( + function () use ($headers) : array { + return $headers; + } + ); + } + + /** + * Create header strategy which generates a numeric array whose size is the number of columns in the given file. + * + * @return HeaderstrategyInterface + */ + public static function none() : HeaderStrategyInterface + { + return new self( + function (SplFileObject $fileObject) : array { + $firstRow = $fileObject->fgetcsv(); + $headers = array_keys($firstRow); + $fileObject->rewind(); + return $headers; + } + ); + } + + /** + * Extracts headers from the given SplFileObject. + * + * @param SplFileObject $fileObject The delimited file containing the headers. + * + * @return array + */ + public function getHeaders(SplFileObject $fileObject) : array + { + return ($this->getHeadersCallable)($fileObject); + } +} diff --git a/src/HeaderStrategyInterface.php b/src/HeaderStrategyInterface.php new file mode 100644 index 0000000..19403ea --- /dev/null +++ b/src/HeaderStrategyInterface.php @@ -0,0 +1,10 @@ + + */ +final class HeaderStrategyTest extends TestCase +{ + /** + * @test + * @covers ::derive + */ + public function derive() + { + $fileObject = $this->getFileObject('pipe_delimited.txt', '|'); + $strategy = HeaderStrategy::derive(); + $this->assertSame( + ['id', 'author', 'title', 'genre', 'price', 'publish_date', 'description'], + $strategy->getHeaders($fileObject) + ); + } + + /** + * @test + * @covers ::provide + */ + public function provide() + { + $headers = ['id', 'author', 'title', 'genre', 'price', 'publish_date', 'description']; + $fileObject = $this->getFileObject('basic.csv'); + $strategy = HeaderStrategy::provide($headers); + $this->assertSame($headers, $strategy->getHeaders($fileObject)); + } + + /** + * @test + * @covers ::none + */ + public function none() + { + $fileObject = $this->getFileObject('no_headers.csv'); + $strategy = HeaderStrategy::none(); + $this->assertSame( + [0, 1, 2, 3, 4, 5, 6], + $strategy->getHeaders($fileObject) + ); + } + + public function getFileObject(string $fileName, string $delimiter = ',') : SplFileObject + { + $fileObject = new SplFileObject(__DIR__ . "/_files/{$fileName}"); + $fileObject->setFlags(SplFileObject::READ_CSV); + $fileObject->setCsvControl($delimiter); + return $fileObject; + } +} From 9dd634d60f321e0469000fe6d86cd9b7d86bf90c Mon Sep 17 00:00:00 2001 From: chadicus Date: Sat, 9 Jun 2018 10:04:42 -0400 Subject: [PATCH 04/18] Use HeaderStrategy in Reader --- src/Reader.php | 61 +++++++++++++++++------------------------ tests/ReaderTest.php | 64 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 83 insertions(+), 42 deletions(-) diff --git a/src/Reader.php b/src/Reader.php index 951a916..2f4a5c7 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -2,6 +2,8 @@ namespace SubjectivePHP\Csv; +use SplFileObject; + /** * Simple class for reading delimited data files */ @@ -10,17 +12,10 @@ class Reader implements \Iterator /** * The column headers. * - * @var array|null + * @var array */ private $headers; - /** - * File pointer to the csv file. - * - * @var resource - */ - private $handle; - /** * The current file pointer position. * @@ -36,21 +31,20 @@ class Reader implements \Iterator private $current = null; /** - * @var CsvOptions + * @var SplFileObject */ - private $csvOptions; + private $fileObject; /** * Create a new Reader instance. * - * @param string $file The full path to the csv file. - * @param array $headers The column headers. If null, the headers will be derived from the first line in - * the file. - * @param CsvOptions $csvOptions Options for the csv file. + * @param string $file The full path to the csv file. + * @param HeaderStrategyInterface $headerStrategy Strategy for obtaining headers of the file. + * @param CsvOptions $csvOptions Options for the csv file. * * @throws \InvalidArgumentException Thrown if $file is not readable. */ - public function __construct($file, array $headers = null, CsvOptions $csvOptions = null) + public function __construct($file, HeaderStrategyInterface $headerStrategy = null, CsvOptions $csvOptions = null) { if (!is_readable((string)$file)) { throw new \InvalidArgumentException( @@ -58,10 +52,18 @@ public function __construct($file, array $headers = null, CsvOptions $csvOptions ); } - $this->csvOptions = $csvOptions ?? new CsvOptions(); + $csvOptions = $csvOptions ?? new CsvOptions(); + $headerStrategy = $headerStrategy ?? HeaderStrategy::derive(); - $this->headers = $headers; - $this->handle = fopen((string)$file, 'r'); + $this->fileObject = new SplFileObject($file); + $this->fileObject->setFlags(SplFileObject::READ_CSV); + $this->fileObject->setCsvControl( + $csvOptions->getDelimiter(), + $csvOptions->getEnclosure(), + $csvOptions->getEscapeChar() + ); + + $this->headers = $headerStrategy->getHeaders($this->fileObject); } /** @@ -78,13 +80,6 @@ public function next() $this->current = array_combine($this->headers, $raw); } - if ($this->headers === null) { - //No headers given, derive from first line of file - $this->headers = $raw; - $this->current = array_combine($this->headers, $this->readLine()); - return; - } - //Headers given, skip first line if header line if ($raw === $this->headers) { $raw = $this->readLine(); @@ -106,13 +101,7 @@ public function next() */ private function readLine() { - $raw = fgetcsv( - $this->handle, - 0, - $this->csvOptions->getDelimiter(), - $this->csvOptions->getEnclosure(), - $this->csvOptions->getEscapeChar() - ); + $raw = $this->fileObject->fgetcsv(); if (empty($raw)) { throw new \Exception('Empty line read'); } @@ -151,7 +140,7 @@ public function key() */ public function rewind() { - rewind($this->handle); + $this->fileObject->rewind(); $this->position = 0; $this->current = null; } @@ -167,7 +156,7 @@ public function valid() $this->next(); } - return !feof($this->handle) && $this->current !== false; + return !$this->fileObject->eof() && $this->current !== false; } /** @@ -177,8 +166,6 @@ public function valid() */ public function __destruct() { - if (is_resource($this->handle)) { - fclose($this->handle); - } + $this->fileObject = null; } } diff --git a/tests/ReaderTest.php b/tests/ReaderTest.php index ec94d51..2ab413a 100644 --- a/tests/ReaderTest.php +++ b/tests/ReaderTest.php @@ -2,6 +2,7 @@ namespace SubjectivePHPTest\Csv; use SubjectivePHP\Csv\CsvOptions; +use SubjectivePHP\Csv\HeaderStrategy; use SubjectivePHP\Csv\Reader; use PHPUnit\Framework\TestCase; @@ -81,6 +82,47 @@ public function basicUsage(Reader $reader) } } + /** + * @test + */ + public function readNoHeaders() + { + $expected = [ + [ + 'bk101', + 'Gambardella, Matthew', + 'XML Developer\'s Guide', + 'Computer', + '44.95', + '2000-10-01', + 'An in-depth look at creating applications with XML.', + ], + [ + 'bk102', + 'Ralls, Kim', + 'Midnight Rain', + 'Fantasy', + '5.95', + '2000-12-16', + 'A former architect battles corporate zombies and an evil sorceress.', + ], + [ + 'bk103', + 'Corets, Eva', + 'Maeve Ascendant', + 'Fantasy', + '5.95', + '2000-11-17', + 'Young survivors lay the foundation for a new society in England.', + ], + ]; + + $reader = new Reader(__DIR__ . '/_files/no_headers.csv', HeaderStrategy::none()); + foreach ($reader as $key => $row) { + $this->assertSame($expected[$key], $row); + } + } + /** * Data provider for basic usage test * @@ -91,10 +133,22 @@ public function getReaders() $headers = ['id', 'author', 'title', 'genre', 'price', 'publish_date', 'description']; return [ [new Reader(__DIR__ . '/_files/basic.csv')], - [new Reader(__DIR__ . '/_files/basic.csv', $headers)], - [new Reader(__DIR__ . '/_files/no_headers.csv', $headers)], - [new Reader(__DIR__ . '/_files/pipe_delimited.txt', $headers, new CsvOptions('|'))], - [new Reader(__DIR__ . '/_files/tab_delimited.txt', $headers, new CsvOptions("\t"))], + [new Reader(__DIR__ . '/_files/basic.csv', HeaderStrategy::provide($headers))], + [new Reader(__DIR__ . '/_files/no_headers.csv', HeaderStrategy::provide($headers))], + [ + new Reader( + __DIR__ . '/_files/pipe_delimited.txt', + HeaderStrategy::provide($headers), + new CsvOptions('|') + ) + ], + [ + new Reader( + __DIR__ . '/_files/tab_delimited.txt', + HeaderStrategy::provide($headers), + new CsvOptions("\t") + ) + ], ]; } @@ -216,7 +270,7 @@ public function getEmptyFiles() return [ [new Reader(__DIR__ . '/_files/empty.csv')], [new Reader(__DIR__ . '/_files/headers_only.csv')], - [new Reader(__DIR__ . '/_files/headers_only.csv', $headers)], + [new Reader(__DIR__ . '/_files/headers_only.csv', HeaderStrategy::provide($headers))], ]; } } From d6e116b1d51226ee9b28c27b5964b436a167f71d Mon Sep 17 00:00:00 2001 From: chadicus Date: Sat, 9 Jun 2018 12:04:10 -0400 Subject: [PATCH 05/18] Add DeriveHeaderStrategy --- src/DeriveHeaderStrategy.php | 30 ++++++++++++++++++++++++++++++ src/HeaderStrategy.php | 8 +------- tests/DeriveHeaderStrategyTest.php | 29 +++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 src/DeriveHeaderStrategy.php create mode 100644 tests/DeriveHeaderStrategyTest.php diff --git a/src/DeriveHeaderStrategy.php b/src/DeriveHeaderStrategy.php new file mode 100644 index 0000000..dfabec8 --- /dev/null +++ b/src/DeriveHeaderStrategy.php @@ -0,0 +1,30 @@ +headers = $fileObject->fgetcsv(); + $fileObject->rewind(); + return $this->headers; + } +} diff --git a/src/HeaderStrategy.php b/src/HeaderStrategy.php index 9a67804..d3d97f9 100644 --- a/src/HeaderStrategy.php +++ b/src/HeaderStrategy.php @@ -23,13 +23,7 @@ private function __construct(callable $getHeadersCallable) */ public static function derive() : HeaderStrategyInterface { - return new self( - function (SplFileObject $fileObject) : array { - $row = $fileObject->fgetcsv(); - $fileObject->rewind(); - return $row; - } - ); + return new DeriveHeaderStrategy(); } /** diff --git a/tests/DeriveHeaderStrategyTest.php b/tests/DeriveHeaderStrategyTest.php new file mode 100644 index 0000000..f9e5573 --- /dev/null +++ b/tests/DeriveHeaderStrategyTest.php @@ -0,0 +1,29 @@ +setFlags(SplFileObject::READ_CSV); + $fileObject->setCsvControl('|'); + $strategy = new DeriveHeaderStrategy(); + $this->assertSame( + ['id', 'author', 'title', 'genre', 'price', 'publish_date', 'description'], + $strategy->getHeaders($fileObject) + ); + } +} From 03a83d468764a81375e94de467778f92a8fa36c0 Mon Sep 17 00:00:00 2001 From: chadicus Date: Sat, 9 Jun 2018 12:09:12 -0400 Subject: [PATCH 06/18] Add NoHeaderStrategy --- src/HeaderStrategy.php | 9 +-------- src/NoHeaderStrategy.php | 19 +++++++++++++++++++ tests/NoHeaderStrategyTest.php | 29 +++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 8 deletions(-) create mode 100644 src/NoHeaderStrategy.php create mode 100644 tests/NoHeaderStrategyTest.php diff --git a/src/HeaderStrategy.php b/src/HeaderStrategy.php index d3d97f9..60d42af 100644 --- a/src/HeaderStrategy.php +++ b/src/HeaderStrategy.php @@ -47,14 +47,7 @@ function () use ($headers) : array { */ public static function none() : HeaderStrategyInterface { - return new self( - function (SplFileObject $fileObject) : array { - $firstRow = $fileObject->fgetcsv(); - $headers = array_keys($firstRow); - $fileObject->rewind(); - return $headers; - } - ); + return new NoHeaderStrategy(); } /** diff --git a/src/NoHeaderStrategy.php b/src/NoHeaderStrategy.php new file mode 100644 index 0000000..c988a35 --- /dev/null +++ b/src/NoHeaderStrategy.php @@ -0,0 +1,19 @@ +fgetcsv(); + $headers = array_keys($firstRow); + $fileObject->rewind(); + return $headers; + } +} diff --git a/tests/NoHeaderStrategyTest.php b/tests/NoHeaderStrategyTest.php new file mode 100644 index 0000000..f30c4de --- /dev/null +++ b/tests/NoHeaderStrategyTest.php @@ -0,0 +1,29 @@ +setFlags(SplFileObject::READ_CSV); + $fileObject->setCsvControl(','); + $strategy = new NoHeaderStrategy(); + $this->assertSame( + [0, 1, 2, 3, 4, 5, 6], + $strategy->getHeaders($fileObject) + ); + } +} From c53c77fda32f01a1fc31fde6593eb0c599568334 Mon Sep 17 00:00:00 2001 From: chadicus Date: Sat, 9 Jun 2018 12:18:06 -0400 Subject: [PATCH 07/18] Add ProvidedHeaderStrategy --- src/HeaderStrategy.php | 6 +----- src/ProvidedHeaderStrategy.php | 26 ++++++++++++++++++++++++++ tests/ProvidedHeaderStrategyTest.php | 28 ++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 src/ProvidedHeaderStrategy.php create mode 100644 tests/ProvidedHeaderStrategyTest.php diff --git a/src/HeaderStrategy.php b/src/HeaderStrategy.php index 60d42af..96c39bf 100644 --- a/src/HeaderStrategy.php +++ b/src/HeaderStrategy.php @@ -33,11 +33,7 @@ public static function derive() : HeaderStrategyInterface */ public static function provide(array $headers) : HeaderStrategyInterface { - return new self( - function () use ($headers) : array { - return $headers; - } - ); + return new ProvidedHeaderStrategy($headers); } /** diff --git a/src/ProvidedHeaderStrategy.php b/src/ProvidedHeaderStrategy.php new file mode 100644 index 0000000..4aa9170 --- /dev/null +++ b/src/ProvidedHeaderStrategy.php @@ -0,0 +1,26 @@ +headers = $headers; + } + + public function getHeaders(SplFileObject $fileObject) : array + { + return $this->headers; + } +} diff --git a/tests/ProvidedHeaderStrategyTest.php b/tests/ProvidedHeaderStrategyTest.php new file mode 100644 index 0000000..6a35e0a --- /dev/null +++ b/tests/ProvidedHeaderStrategyTest.php @@ -0,0 +1,28 @@ +setFlags(SplFileObject::READ_CSV); + $fileObject->setCsvControl(','); + $strategy = new ProvidedHeaderStrategy($headers); + $this->assertSame($headers, $strategy->getHeaders($fileObject)); + } +} From 0cbc060b4a0a5ee8829ad5d1d7c11e6ba67965dd Mon Sep 17 00:00:00 2001 From: chadicus Date: Sat, 9 Jun 2018 12:35:06 -0400 Subject: [PATCH 08/18] Remove HeaderStrategy --- src/HeaderStrategy.php | 60 ---------------------------------- src/Reader.php | 2 +- tests/HeaderStrategyTest.php | 63 ------------------------------------ tests/ReaderTest.php | 16 +++++---- 4 files changed, 10 insertions(+), 131 deletions(-) delete mode 100644 src/HeaderStrategy.php delete mode 100644 tests/HeaderStrategyTest.php diff --git a/src/HeaderStrategy.php b/src/HeaderStrategy.php deleted file mode 100644 index 96c39bf..0000000 --- a/src/HeaderStrategy.php +++ /dev/null @@ -1,60 +0,0 @@ -getHeadersCallable = $getHeadersCallable; - } - - /** - * Create header strategy which derives the headers from the first line of the file. - * - * @return HeaderstrategyInterface - */ - public static function derive() : HeaderStrategyInterface - { - return new DeriveHeaderStrategy(); - } - - /** - * Create header strategy which uses the provided headers array. - * - * @return HeaderstrategyInterface - */ - public static function provide(array $headers) : HeaderStrategyInterface - { - return new ProvidedHeaderStrategy($headers); - } - - /** - * Create header strategy which generates a numeric array whose size is the number of columns in the given file. - * - * @return HeaderstrategyInterface - */ - public static function none() : HeaderStrategyInterface - { - return new NoHeaderStrategy(); - } - - /** - * Extracts headers from the given SplFileObject. - * - * @param SplFileObject $fileObject The delimited file containing the headers. - * - * @return array - */ - public function getHeaders(SplFileObject $fileObject) : array - { - return ($this->getHeadersCallable)($fileObject); - } -} diff --git a/src/Reader.php b/src/Reader.php index 2f4a5c7..b49c097 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -53,7 +53,7 @@ public function __construct($file, HeaderStrategyInterface $headerStrategy = nul } $csvOptions = $csvOptions ?? new CsvOptions(); - $headerStrategy = $headerStrategy ?? HeaderStrategy::derive(); + $headerStrategy = $headerStrategy ?? new DeriveHeaderStrategy(); $this->fileObject = new SplFileObject($file); $this->fileObject->setFlags(SplFileObject::READ_CSV); diff --git a/tests/HeaderStrategyTest.php b/tests/HeaderStrategyTest.php deleted file mode 100644 index 2b0e372..0000000 --- a/tests/HeaderStrategyTest.php +++ /dev/null @@ -1,63 +0,0 @@ - - */ -final class HeaderStrategyTest extends TestCase -{ - /** - * @test - * @covers ::derive - */ - public function derive() - { - $fileObject = $this->getFileObject('pipe_delimited.txt', '|'); - $strategy = HeaderStrategy::derive(); - $this->assertSame( - ['id', 'author', 'title', 'genre', 'price', 'publish_date', 'description'], - $strategy->getHeaders($fileObject) - ); - } - - /** - * @test - * @covers ::provide - */ - public function provide() - { - $headers = ['id', 'author', 'title', 'genre', 'price', 'publish_date', 'description']; - $fileObject = $this->getFileObject('basic.csv'); - $strategy = HeaderStrategy::provide($headers); - $this->assertSame($headers, $strategy->getHeaders($fileObject)); - } - - /** - * @test - * @covers ::none - */ - public function none() - { - $fileObject = $this->getFileObject('no_headers.csv'); - $strategy = HeaderStrategy::none(); - $this->assertSame( - [0, 1, 2, 3, 4, 5, 6], - $strategy->getHeaders($fileObject) - ); - } - - public function getFileObject(string $fileName, string $delimiter = ',') : SplFileObject - { - $fileObject = new SplFileObject(__DIR__ . "/_files/{$fileName}"); - $fileObject->setFlags(SplFileObject::READ_CSV); - $fileObject->setCsvControl($delimiter); - return $fileObject; - } -} diff --git a/tests/ReaderTest.php b/tests/ReaderTest.php index 2ab413a..71e0b25 100644 --- a/tests/ReaderTest.php +++ b/tests/ReaderTest.php @@ -2,7 +2,9 @@ namespace SubjectivePHPTest\Csv; use SubjectivePHP\Csv\CsvOptions; -use SubjectivePHP\Csv\HeaderStrategy; +use SubjectivePHP\Csv\DeriveHeaderStrategy; +use SubjectivePHP\Csv\NoHeaderStrategy; +use SubjectivePHP\Csv\ProvidedHeaderStrategy; use SubjectivePHP\Csv\Reader; use PHPUnit\Framework\TestCase; @@ -117,7 +119,7 @@ public function readNoHeaders() ], ]; - $reader = new Reader(__DIR__ . '/_files/no_headers.csv', HeaderStrategy::none()); + $reader = new Reader(__DIR__ . '/_files/no_headers.csv', new NoHeaderStrategy()); foreach ($reader as $key => $row) { $this->assertSame($expected[$key], $row); } @@ -133,19 +135,19 @@ public function getReaders() $headers = ['id', 'author', 'title', 'genre', 'price', 'publish_date', 'description']; return [ [new Reader(__DIR__ . '/_files/basic.csv')], - [new Reader(__DIR__ . '/_files/basic.csv', HeaderStrategy::provide($headers))], - [new Reader(__DIR__ . '/_files/no_headers.csv', HeaderStrategy::provide($headers))], + [new Reader(__DIR__ . '/_files/basic.csv', new ProvidedHeaderStrategy($headers))], + [new Reader(__DIR__ . '/_files/no_headers.csv', new ProvidedHeaderStrategy($headers))], [ new Reader( __DIR__ . '/_files/pipe_delimited.txt', - HeaderStrategy::provide($headers), + new ProvidedHeaderStrategy($headers), new CsvOptions('|') ) ], [ new Reader( __DIR__ . '/_files/tab_delimited.txt', - HeaderStrategy::provide($headers), + new ProvidedHeaderStrategy($headers), new CsvOptions("\t") ) ], @@ -270,7 +272,7 @@ public function getEmptyFiles() return [ [new Reader(__DIR__ . '/_files/empty.csv')], [new Reader(__DIR__ . '/_files/headers_only.csv')], - [new Reader(__DIR__ . '/_files/headers_only.csv', HeaderStrategy::provide($headers))], + [new Reader(__DIR__ . '/_files/headers_only.csv', new ProvidedHeaderStrategy($headers))], ]; } } From e5d6e554b76016b0806d7de9ecffab9dd2bf7dec Mon Sep 17 00:00:00 2001 From: chadicus Date: Sat, 9 Jun 2018 13:30:27 -0400 Subject: [PATCH 09/18] Add HeaderStrategy::isHeaderRow --- src/DeriveHeaderStrategy.php | 5 ++++ src/HeaderStrategyInterface.php | 2 ++ src/NoHeaderStrategy.php | 5 ++++ src/ProvidedHeaderStrategy.php | 5 ++++ tests/DeriveHeaderStrategyTest.php | 38 +++++++++++++++++++++++--- tests/NoHeaderStrategyTest.php | 14 ++++++++++ tests/ProvidedHeaderStrategyTest.php | 40 +++++++++++++++++++++++++--- 7 files changed, 103 insertions(+), 6 deletions(-) diff --git a/src/DeriveHeaderStrategy.php b/src/DeriveHeaderStrategy.php index dfabec8..eeb4834 100644 --- a/src/DeriveHeaderStrategy.php +++ b/src/DeriveHeaderStrategy.php @@ -27,4 +27,9 @@ public function getHeaders(SplFileObject $fileObject) : array $fileObject->rewind(); return $this->headers; } + + public function isHeaderRow(array $row) : bool + { + return $row === $this->headers; + } } diff --git a/src/HeaderStrategyInterface.php b/src/HeaderStrategyInterface.php index 19403ea..7315ef0 100644 --- a/src/HeaderStrategyInterface.php +++ b/src/HeaderStrategyInterface.php @@ -7,4 +7,6 @@ interface HeaderStrategyInterface { public function getHeaders(SplFileObject $fileObject) : array; + + public function isHeaderRow(array $row) : bool; } diff --git a/src/NoHeaderStrategy.php b/src/NoHeaderStrategy.php index c988a35..d51b8df 100644 --- a/src/NoHeaderStrategy.php +++ b/src/NoHeaderStrategy.php @@ -16,4 +16,9 @@ public function getHeaders(SplFileObject $fileObject) : array $fileObject->rewind(); return $headers; } + + public function isHeaderRow(array $row) : bool + { + return false; + } } diff --git a/src/ProvidedHeaderStrategy.php b/src/ProvidedHeaderStrategy.php index 4aa9170..6c452dd 100644 --- a/src/ProvidedHeaderStrategy.php +++ b/src/ProvidedHeaderStrategy.php @@ -23,4 +23,9 @@ public function getHeaders(SplFileObject $fileObject) : array { return $this->headers; } + + public function isHeaderRow(array $row) : bool + { + return $row === $this->headers; + } } diff --git a/tests/DeriveHeaderStrategyTest.php b/tests/DeriveHeaderStrategyTest.php index f9e5573..6cc6685 100644 --- a/tests/DeriveHeaderStrategyTest.php +++ b/tests/DeriveHeaderStrategyTest.php @@ -17,13 +17,45 @@ final class DeriveHeaderStrategyTest extends TestCase */ public function getHeaders() { - $fileObject = new SplFileObject(__DIR__ . '/_files/pipe_delimited.txt'); - $fileObject->setFlags(SplFileObject::READ_CSV); - $fileObject->setCsvControl('|'); $strategy = new DeriveHeaderStrategy(); + $fileObject = $this->getFileObject(); + $headers = $strategy->getHeaders($fileObject); $this->assertSame( ['id', 'author', 'title', 'genre', 'price', 'publish_date', 'description'], $strategy->getHeaders($fileObject) ); } + + /** + * @test + * @covers ::isHeaderRow + */ + public function rowIsHeaderRow() + { + $strategy = new DeriveHeaderStrategy(); + $fileObject = $this->getFileObject(); + $headers = $strategy->getHeaders($fileObject); + $this->assertTrue($strategy->isHeaderRow($fileObject->fgetcsv())); + } + + /** + * @test + * @covers ::isHeaderRow + */ + public function rowNotIsHeaderRow() + { + $strategy = new DeriveHeaderStrategy(); + $fileObject = $this->getFileObject(); + $headers = $strategy->getHeaders($fileObject); + $fileObject->fgetcsv(); + $this->assertFalse($strategy->isHeaderRow($fileObject->fgetcsv())); + } + + private function getFileObject() : SplFileObject + { + $fileObject = new SplFileObject(__DIR__ . '/_files/pipe_delimited.txt'); + $fileObject->setFlags(SplFileObject::READ_CSV); + $fileObject->setCsvControl('|'); + return $fileObject; + } } diff --git a/tests/NoHeaderStrategyTest.php b/tests/NoHeaderStrategyTest.php index f30c4de..ef77177 100644 --- a/tests/NoHeaderStrategyTest.php +++ b/tests/NoHeaderStrategyTest.php @@ -26,4 +26,18 @@ public function getHeaders() $strategy->getHeaders($fileObject) ); } + + /** + * @test + * @covers ::isHeaderRow + */ + public function isHeaderRowAlwaysReturnsFalse() + { + $fileObject = new SplFileObject(__DIR__ . '/_files/no_headers.csv'); + $fileObject->setFlags(SplFileObject::READ_CSV); + $fileObject->setCsvControl(','); + $strategy = new NoHeaderStrategy(); + $this->assertFalse($strategy->isHeaderRow($fileObject->fgetcsv())); + $this->assertFalse($strategy->isHeaderRow($fileObject->fgetcsv())); + } } diff --git a/tests/ProvidedHeaderStrategyTest.php b/tests/ProvidedHeaderStrategyTest.php index 6a35e0a..7409e78 100644 --- a/tests/ProvidedHeaderStrategyTest.php +++ b/tests/ProvidedHeaderStrategyTest.php @@ -12,17 +12,51 @@ */ final class ProvidedHeaderStrategyTest extends TestCase { + const HEADERS = ['id', 'author', 'title', 'genre', 'price', 'publish_date', 'description']; + /** * @test * @covers ::getHeaders */ public function getHeaders() { - $headers = ['id', 'author', 'title', 'genre', 'price', 'publish_date', 'description']; + $fileObject = $this->getFileObject(); + $strategy = $this->getStrategy(); + $this->assertSame(self::HEADERS, $strategy->getHeaders($fileObject)); + } + + /** + * @test + * @covers ::isHeaderRow + */ + public function rowIsHeaderRow() + { + $strategy = $this->getStrategy(); + $this->assertTrue($strategy->isHeaderRow(self::HEADERS)); + } + + /** + * @test + * @covers ::isHeaderRow + */ + public function rowIsNotHeaderRow() + { + $strategy = $this->getStrategy(); + $fileObject = $this->getFileObject(); + $fileObject->fgetcsv(); + $this->assertFalse($strategy->isHeaderRow($fileObject->fgetcsv())); + } + + private function getFileObject() : SplFileObject + { $fileObject = new SplFileObject(__DIR__ . '/_files/basic.csv'); $fileObject->setFlags(SplFileObject::READ_CSV); $fileObject->setCsvControl(','); - $strategy = new ProvidedHeaderStrategy($headers); - $this->assertSame($headers, $strategy->getHeaders($fileObject)); + return $fileObject; + } + + private function getStrategy() : ProvidedHeaderStrategy + { + return new ProvidedHeaderStrategy(self::HEADERS); } } From 804c9d78369da0178a6b2f6443fb02a5a1e0e392 Mon Sep 17 00:00:00 2001 From: chadicus Date: Sat, 9 Jun 2018 13:33:35 -0400 Subject: [PATCH 10/18] Use isHeaderRow in Reader --- src/Reader.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Reader.php b/src/Reader.php index b49c097..fe21163 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -35,6 +35,11 @@ class Reader implements \Iterator */ private $fileObject; + /** + * @var HeaderStrategyInterface + */ + private $headerStrategy; + /** * Create a new Reader instance. * @@ -53,7 +58,7 @@ public function __construct($file, HeaderStrategyInterface $headerStrategy = nul } $csvOptions = $csvOptions ?? new CsvOptions(); - $headerStrategy = $headerStrategy ?? new DeriveHeaderStrategy(); + $this->headerStrategy = $headerStrategy ?? new DeriveHeaderStrategy(); $this->fileObject = new SplFileObject($file); $this->fileObject->setFlags(SplFileObject::READ_CSV); @@ -63,7 +68,7 @@ public function __construct($file, HeaderStrategyInterface $headerStrategy = nul $csvOptions->getEscapeChar() ); - $this->headers = $headerStrategy->getHeaders($this->fileObject); + $this->headers = $this->headerStrategy->getHeaders($this->fileObject); } /** @@ -81,7 +86,7 @@ public function next() } //Headers given, skip first line if header line - if ($raw === $this->headers) { + if ($this->headerStrategy->isHeaderRow($raw)) { $raw = $this->readLine(); } From d4b12282aa49e40b6f0388ddec8dec1c0f7c8eee Mon Sep 17 00:00:00 2001 From: chadicus Date: Sat, 9 Jun 2018 14:40:24 -0400 Subject: [PATCH 11/18] Add HeaderStrategyInterface::createDataRow --- src/DeriveHeaderStrategy.php | 5 +++++ src/HeaderStrategyInterface.php | 2 ++ src/NoHeaderStrategy.php | 5 +++++ src/ProvidedHeaderStrategy.php | 5 +++++ 4 files changed, 17 insertions(+) diff --git a/src/DeriveHeaderStrategy.php b/src/DeriveHeaderStrategy.php index eeb4834..0d4ffd9 100644 --- a/src/DeriveHeaderStrategy.php +++ b/src/DeriveHeaderStrategy.php @@ -32,4 +32,9 @@ public function isHeaderRow(array $row) : bool { return $row === $this->headers; } + + public function createDataRow(array $row) : array + { + return array_combine($this->headers, $row); + } } diff --git a/src/HeaderStrategyInterface.php b/src/HeaderStrategyInterface.php index 7315ef0..65a86ed 100644 --- a/src/HeaderStrategyInterface.php +++ b/src/HeaderStrategyInterface.php @@ -9,4 +9,6 @@ interface HeaderStrategyInterface public function getHeaders(SplFileObject $fileObject) : array; public function isHeaderRow(array $row) : bool; + + public function createDataRow(array $row) : array; } diff --git a/src/NoHeaderStrategy.php b/src/NoHeaderStrategy.php index d51b8df..359696b 100644 --- a/src/NoHeaderStrategy.php +++ b/src/NoHeaderStrategy.php @@ -21,4 +21,9 @@ public function isHeaderRow(array $row) : bool { return false; } + + public function createDataRow(array $row) : array + { + return $row; + } } diff --git a/src/ProvidedHeaderStrategy.php b/src/ProvidedHeaderStrategy.php index 6c452dd..8cb9db9 100644 --- a/src/ProvidedHeaderStrategy.php +++ b/src/ProvidedHeaderStrategy.php @@ -28,4 +28,9 @@ public function isHeaderRow(array $row) : bool { return $row === $this->headers; } + + public function createDataRow(array $row) : array + { + return array_combine($this->headers, $row); + } } From e2fb37a915320831f973ec62fd1edd214a42b158 Mon Sep 17 00:00:00 2001 From: chadicus Date: Sat, 9 Jun 2018 14:40:54 -0400 Subject: [PATCH 12/18] Use createDataRow in Reader --- src/Reader.php | 4 +-- tests/ReaderTest.php | 59 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/Reader.php b/src/Reader.php index fe21163..036e5b8 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -82,7 +82,7 @@ public function next() $raw = $this->readLine(); if ($this->current !== null) { ++$this->position; - $this->current = array_combine($this->headers, $raw); + $this->current = $this->headerStrategy->createDataRow($raw); } //Headers given, skip first line if header line @@ -90,7 +90,7 @@ public function next() $raw = $this->readLine(); } - $this->current = array_combine($this->headers, $raw); + $this->current = $this->headerStrategy->createDataRow($raw); } catch (\Exception $e) { $this->current = false; return false; diff --git a/tests/ReaderTest.php b/tests/ReaderTest.php index 71e0b25..5749f5d 100644 --- a/tests/ReaderTest.php +++ b/tests/ReaderTest.php @@ -3,6 +3,7 @@ use SubjectivePHP\Csv\CsvOptions; use SubjectivePHP\Csv\DeriveHeaderStrategy; +use SubjectivePHP\Csv\MappedHeaderStrategy; use SubjectivePHP\Csv\NoHeaderStrategy; use SubjectivePHP\Csv\ProvidedHeaderStrategy; use SubjectivePHP\Csv\Reader; @@ -84,6 +85,64 @@ public function basicUsage(Reader $reader) } } + /** + * @test + * @covers ::next + * @covers ::current + * @covers ::key + * @covers ::valid + * @covers ::rewind + */ + public function readWithCustomHeaders() + { + $expected = [ + [ + 'Book ID' => 'bk101', + 'Author' => 'Gambardella, Matthew', + 'Title' => 'XML Developer\'s Guide', + 'Genre' => 'Computer', + 'Price' => '44.95', + 'Publish Date' => '2000-10-01', + 'Description' => 'An in-depth look at creating applications with XML.', + ], + [ + 'Book ID' => 'bk102', + 'Author' => 'Ralls, Kim', + 'Title' => 'Midnight Rain', + 'Genre' => 'Fantasy', + 'Price' => '5.95', + 'Publish Date' => '2000-12-16', + 'Description' => 'A former architect battles corporate zombies and an evil sorceress.', + ], + [ + 'Book ID' => 'bk103', + 'Author' => 'Corets, Eva', + 'Title' => 'Maeve Ascendant', + 'Genre' => 'Fantasy', + 'Price' => '5.95', + 'Publish Date' => '2000-11-17', + 'Description' => 'Young survivors lay the foundation for a new society in England.', + ], + ]; + + $strategy = new MappedHeaderStrategy( + [ + 'id' => 'Book ID', + 'author' => 'Author', + 'title' => 'Title', + 'genre' => 'Genre', + 'price' => 'Price', + 'publish_date' => 'Publish Date', + 'description' => 'Description', + ] + ); + + $reader = new Reader(__DIR__ . '/_files/basic.csv', $strategy); + foreach ($reader as $key => $row) { + $this->assertSame($expected[$key], $row); + } + } + /** * @test */ From d29e23733ba97e57e6693f950a628e7158412a67 Mon Sep 17 00:00:00 2001 From: chadicus Date: Sat, 9 Jun 2018 17:46:56 -0400 Subject: [PATCH 13/18] Add MappedHeaderStrategy --- src/MappedHeaderStrategy.php | 46 +++++++++++++ tests/MappedHeaderStrategyTest.php | 100 +++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 src/MappedHeaderStrategy.php create mode 100644 tests/MappedHeaderStrategyTest.php diff --git a/src/MappedHeaderStrategy.php b/src/MappedHeaderStrategy.php new file mode 100644 index 0000000..b43840e --- /dev/null +++ b/src/MappedHeaderStrategy.php @@ -0,0 +1,46 @@ +headerMap = $headerMap; + } + + public function getHeaders(SplFileObject $fileObject) : array + { + return array_values($this->headerMap); + } + + public function isHeaderRow(array $row) : bool + { + $headers = array_keys($this->headerMap); + sort($row); + sort($headers); + return $row === $headers; + } + + public function createDataRow(array $row) : array + { + $result = []; + $originalHeaders = array_keys($this->headerMap); + foreach ($originalHeaders as $index => $key) { + $newHeader = $this->headerMap[$key]; + $result[$newHeader] = $row[$index] ?? null; + } + + return $result; + } +} diff --git a/tests/MappedHeaderStrategyTest.php b/tests/MappedHeaderStrategyTest.php new file mode 100644 index 0000000..2d3dbe7 --- /dev/null +++ b/tests/MappedHeaderStrategyTest.php @@ -0,0 +1,100 @@ + 'Book ID', + 'author' => 'Author', + 'title' => 'Title', + 'genre' => 'Genre', + 'price' => 'Price', + 'publish_date' => 'Publish Date', + 'description' => 'Description', + ]; + + /** + * @test + * @covers ::getHeaders + */ + public function getHeaders() + { + $fileObject = $this->getFileObject(); + $strategy = $this->getStrategy(); + $this->assertSame(array_values(self::HEADER_MAP), $strategy->getHeaders($fileObject)); + } + + /** + * @test + * @covers ::isHeaderRow + */ + public function rowIsHeaderRow() + { + $strategy = $this->getStrategy(); + $this->assertTrue($strategy->isHeaderRow(array_keys(self::HEADER_MAP))); + } + + /** + * @test + * @covers ::isHeaderRow + */ + public function rowIsNotHeaderRow() + { + $strategy = $this->getStrategy(); + $fileObject = $this->getFileObject(); + $fileObject->fgetcsv(); + $this->assertFalse($strategy->isHeaderRow($fileObject->fgetcsv())); + } + + /** + * @test + * @covers ::createDataRow + */ + public function createDataRow() + { + $row = [ + 'bk101', + 'Gambardella, Matthew', + 'XML Developer\'s Guide', + 'Computer', + '44.95', + '2000-10-01', + 'An in-depth look at creating applications with XML.', + ]; + $strategy = $this->getStrategy(); + $this->assertSame( + [ + 'Book ID' => 'bk101', + 'Author' => 'Gambardella, Matthew', + 'Title' => 'XML Developer\'s Guide', + 'Genre' => 'Computer', + 'Price' => '44.95', + 'Publish Date' => '2000-10-01', + 'Description' => 'An in-depth look at creating applications with XML.', + ], + $strategy->createDataRow($row) + ); + } + + private function getFileObject() : SplFileObject + { + $fileObject = new SplFileObject(__DIR__ . '/_files/basic.csv'); + $fileObject->setFlags(SplFileObject::READ_CSV); + $fileObject->setCsvControl(','); + return $fileObject; + } + + private function getStrategy() : MappedHeaderStrategy + { + return new MappedHeaderStrategy(self::HEADER_MAP); + } +} From 0dfd36d4ca6ad9d0025f8703d21ec9ce3d3e0d29 Mon Sep 17 00:00:00 2001 From: chadicus Date: Sat, 9 Jun 2018 17:48:18 -0400 Subject: [PATCH 14/18] Fix bug for empty file --- src/DeriveHeaderStrategy.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DeriveHeaderStrategy.php b/src/DeriveHeaderStrategy.php index 0d4ffd9..458ef23 100644 --- a/src/DeriveHeaderStrategy.php +++ b/src/DeriveHeaderStrategy.php @@ -23,7 +23,7 @@ final class DeriveHeaderStrategy implements HeaderStrategyInterface */ public function getHeaders(SplFileObject $fileObject) : array { - $this->headers = $fileObject->fgetcsv(); + $this->headers = $fileObject->fgetcsv() ?? []; $fileObject->rewind(); return $this->headers; } From d6bc2191c708559f40b8097794b2161e695116e6 Mon Sep 17 00:00:00 2001 From: chadicus Date: Sat, 9 Jun 2018 18:05:16 -0400 Subject: [PATCH 15/18] Update Reader to implement IteratorAggregate --- src/Reader.php | 170 ++++++++++++------------------------------- tests/ReaderTest.php | 122 ++++++------------------------- 2 files changed, 70 insertions(+), 222 deletions(-) diff --git a/src/Reader.php b/src/Reader.php index 036e5b8..7eb57f5 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -7,29 +7,8 @@ /** * Simple class for reading delimited data files */ -class Reader implements \Iterator +class Reader implements \IteratorAggregate { - /** - * The column headers. - * - * @var array - */ - private $headers; - - /** - * The current file pointer position. - * - * @var integer - */ - private $position = 0; - - /** - * The current row within the csv file. - * - * @var array|false|null - */ - private $current = null; - /** * @var SplFileObject */ @@ -49,128 +28,71 @@ class Reader implements \Iterator * * @throws \InvalidArgumentException Thrown if $file is not readable. */ - public function __construct($file, HeaderStrategyInterface $headerStrategy = null, CsvOptions $csvOptions = null) + public function __construct(string $file, HeaderStrategyInterface $headerStrategy = null, CsvOptions $csvOptions = null) { - if (!is_readable((string)$file)) { - throw new \InvalidArgumentException( - '$file must be a string containing a full path to a readable delimited file' - ); - } - - $csvOptions = $csvOptions ?? new CsvOptions(); + $this->fileObject = $this->getFileObject($file, $csvOptions ?? new CsvOptions()); $this->headerStrategy = $headerStrategy ?? new DeriveHeaderStrategy(); - - $this->fileObject = new SplFileObject($file); - $this->fileObject->setFlags(SplFileObject::READ_CSV); - $this->fileObject->setCsvControl( - $csvOptions->getDelimiter(), - $csvOptions->getEnclosure(), - $csvOptions->getEscapeChar() - ); - - $this->headers = $this->headerStrategy->getHeaders($this->fileObject); + $this->headerStrategy->getHeaders($this->fileObject); } - /** - * Advances to the next row in this csv reader - * - * @return mixed - */ - public function next() + public function getIterator() : \Traversable { - try { - $raw = $this->readLine(); - if ($this->current !== null) { - ++$this->position; - $this->current = $this->headerStrategy->createDataRow($raw); - } - - //Headers given, skip first line if header line - if ($this->headerStrategy->isHeaderRow($raw)) { - $raw = $this->readLine(); - } - - $this->current = $this->headerStrategy->createDataRow($raw); - } catch (\Exception $e) { - $this->current = false; - return false; - } + return $this->getOuterIterator( + $this->getInnerIterator() + ); } - /** - * Helper method to read the next line in the delimited file. - * - * @return array|false - * - * @throws \Exception Thrown if no data is returned when reading the file. - */ - private function readLine() + private function getFileObject(string $filePath, CsvOptions $csvOptions) : SplFileObject { - $raw = $this->fileObject->fgetcsv(); - if (empty($raw)) { - throw new \Exception('Empty line read'); + if (!is_readable($filePath)) { + throw new \InvalidArgumentException( + '$file must be a string containing a full path to a readable delimited file' + ); } - return $raw; - } - - /** - * Return the current element. - * - * @return array returns array containing values from the current row - */ - public function current() - { - if ($this->current === null) { - $this->next(); - } + $fileObject = new SplFileObject($filePath); + $fileObject->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY); + $fileObject->setCsvControl( + $csvOptions->getDelimiter(), + $csvOptions->getEnclosure(), + $csvOptions->getEscapeChar() + ); - return $this->current; + return $fileObject; } - /** - * Return the key of the current element. - * - * @return integer - */ - public function key() + public function __destruct() { - return $this->position; + $this->fileObject = null; } - /** - * Rewind the Iterator to the first element. - * - * @return void - */ - public function rewind() + private function getInnerIterator() : \CallbackFilterIterator { - $this->fileObject->rewind(); - $this->position = 0; - $this->current = null; + $strategy = $this->headerStrategy; + return new \CallbackFilterIterator( + $this->fileObject, + function ($current) use ($strategy) { + return !$strategy->isHeaderRow($current); + } + ); } - /** - * Check if there is a current element after calls to rewind() or next(). - * - * @return bool true if there is a current element, false otherwise - */ - public function valid() + private function getOuterIterator(\CallbackFilterIterator $innerIterator) : \IteratorIterator { - if ($this->current === null) { - $this->next(); - } - - return !$this->fileObject->eof() && $this->current !== false; - } + return new class($innerIterator, $this->headerStrategy) extends \IteratorIterator + { + private $strategy; + + public function __construct(\Traversable $innerIterator, HeaderStrategyInterface $strategy) + { + parent::__construct($innerIterator); + $this->strategy = $strategy; + } - /** - * Ensure file handles are closed when all references to this reader are destroyed. - * - * @return void - */ - public function __destruct() - { - $this->fileObject = null; + public function current() + { + return $this->strategy->createDataRow(parent::current()); + } + }; } } diff --git a/tests/ReaderTest.php b/tests/ReaderTest.php index 5749f5d..0ce73ac 100644 --- a/tests/ReaderTest.php +++ b/tests/ReaderTest.php @@ -19,29 +19,11 @@ */ final class ReaderTest extends TestCase { - private $unreadableFilePath; - - public function setUp() - { - $this->unreadableFilePath = tempnam(sys_get_temp_dir(), 'csv'); - touch($this->unreadableFilePath); - chmod($this->unreadableFilePath, 0220); - } - - public function tearDown() - { - unlink($this->unreadableFilePath); - } - /** * Verify basic usage of Reader. * * @test - * @covers ::next - * @covers ::current - * @covers ::key - * @covers ::valid - * @covers ::rewind + * @covers ::getIterator * @dataProvider getReaders() * * @param Reader $reader The Reader instance to use in the test. @@ -80,18 +62,12 @@ public function basicUsage(Reader $reader) ], ]; - foreach ($reader as $key => $row) { - $this->assertSame($expected[$key], $row); - } + $this->assertSame($expected, array_values(iterator_to_array($reader))); } /** * @test - * @covers ::next - * @covers ::current - * @covers ::key - * @covers ::valid - * @covers ::rewind + * @covers ::getIterator */ public function readWithCustomHeaders() { @@ -138,13 +114,12 @@ public function readWithCustomHeaders() ); $reader = new Reader(__DIR__ . '/_files/basic.csv', $strategy); - foreach ($reader as $key => $row) { - $this->assertSame($expected[$key], $row); - } + $this->assertSame($expected, array_values(iterator_to_array($reader))); } /** * @test + * @covers ::getIterator */ public function readNoHeaders() { @@ -179,9 +154,7 @@ public function readNoHeaders() ]; $reader = new Reader(__DIR__ . '/_files/no_headers.csv', new NoHeaderStrategy()); - foreach ($reader as $key => $row) { - $this->assertSame($expected[$key], $row); - } + $this->assertSame($expected, array_values(iterator_to_array($reader))); } /** @@ -222,85 +195,40 @@ public function getReaders() * @covers ::__construct * @expectedException \InvalidArgumentException * @expectedExceptionMessage $file must be a string containing a full path to a readable delimited file - * @dataProvider getFiles * * @return void */ - public function constructInvalidFileParam($file) + public function constructWithFileThatDoesNotExist() { - $reader = new Reader($file); + new Reader(__DIR__ . '/_files/not_found.csv'); } /** - * Data provider for constructInvalidFileParam() test. - * - * @return array - */ - public function getFiles() - { - return [ - [__DIR__ . '/_files/not_readable.csv'], - [true], - [null], - [__DIR__ . '/_files/doesnotexist.csv'], - ]; - } - - /** - * Verify behaviour of consecutive rewind(). - * * @test - * @covers ::rewind - * - * @return void + * @covers ::__construct */ - public function consecutiveRewind() + public function constructWithUnreadableFile() { - $reader = new Reader(__DIR__ . '/_files/basic.csv'); - $count = 0; - foreach ($reader as $row) { - $count++; + try { + $unreadableFilePath = tempnam(sys_get_temp_dir(), 'csv'); + touch($unreadableFilePath); + chmod($unreadableFilePath, 0220); + new Reader($unreadableFilePath); + } catch (\InvalidArgumentException $e) { + $this->assertSame( + '$file must be a string containing a full path to a readable delimited file', + $e->getMessage() + ); + } finally { + unlink($unreadableFilePath); } - - $reader->rewind(); - $reader->rewind(); - $this->assertSame(0, $reader->key()); - } - - /** - * Verify basic behaviour of current(). - * - * @test - * @covers ::current - * - * @return void - */ - public function current() - { - $reader = new Reader(__DIR__ . '/_files/basic.csv'); - $this->assertSame( - [ - 'id' => 'bk101', - 'author' => 'Gambardella, Matthew', - 'title' => 'XML Developer\'s Guide', - 'genre' => 'Computer', - 'price' => '44.95', - 'publish_date' => '2000-10-01', - 'description' => 'An in-depth look at creating applications with XML.', - ], - $reader->current() - ); } /** * Verify behavior of Reader with an empty file * * @test - * @covers ::next - * @covers ::current - * @covers ::key - * @covers ::valid - * @covers ::rewind + * @covers ::getIterator * @dataProvider getEmptyFiles * * @param Reader $reader The reader instance to use in the tests. @@ -311,10 +239,8 @@ public function emptyFiles(Reader $reader) { $total = 0; - $reader->rewind(); - while ($reader->valid()) { + foreach ($reader as $row) { $total++; - $reader->next(); } $this->assertSame(0, $total); From a44d72ba5013c94c8d74194eaf25bdd752ed3265 Mon Sep 17 00:00:00 2001 From: chadicus Date: Sat, 9 Jun 2018 18:12:44 -0400 Subject: [PATCH 16/18] Remove unused code from DeriveHeaderStrategyTest --- tests/DeriveHeaderStrategyTest.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/DeriveHeaderStrategyTest.php b/tests/DeriveHeaderStrategyTest.php index 6cc6685..8a1553c 100644 --- a/tests/DeriveHeaderStrategyTest.php +++ b/tests/DeriveHeaderStrategyTest.php @@ -19,7 +19,6 @@ public function getHeaders() { $strategy = new DeriveHeaderStrategy(); $fileObject = $this->getFileObject(); - $headers = $strategy->getHeaders($fileObject); $this->assertSame( ['id', 'author', 'title', 'genre', 'price', 'publish_date', 'description'], $strategy->getHeaders($fileObject) @@ -34,7 +33,7 @@ public function rowIsHeaderRow() { $strategy = new DeriveHeaderStrategy(); $fileObject = $this->getFileObject(); - $headers = $strategy->getHeaders($fileObject); + $strategy->getHeaders($fileObject); $this->assertTrue($strategy->isHeaderRow($fileObject->fgetcsv())); } @@ -46,7 +45,7 @@ public function rowNotIsHeaderRow() { $strategy = new DeriveHeaderStrategy(); $fileObject = $this->getFileObject(); - $headers = $strategy->getHeaders($fileObject); + $strategy->getHeaders($fileObject); $fileObject->fgetcsv(); $this->assertFalse($strategy->isHeaderRow($fileObject->fgetcsv())); } From ca822d23096fd782c8ccd264e9531c14cc24eacd Mon Sep 17 00:00:00 2001 From: chadicus Date: Sat, 9 Jun 2018 18:38:46 -0400 Subject: [PATCH 17/18] Remove invald parameter doc --- tests/ReaderTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/ReaderTest.php b/tests/ReaderTest.php index 0ce73ac..2eb2303 100644 --- a/tests/ReaderTest.php +++ b/tests/ReaderTest.php @@ -189,8 +189,6 @@ public function getReaders() /** * Verify parameter checks for $file in __construct(). * - * @param mixed $file The file parameter to check. - * * @test * @covers ::__construct * @expectedException \InvalidArgumentException From c2ffdb8b93f47e2e96b9c20fd385ab3e8aa8d5e9 Mon Sep 17 00:00:00 2001 From: chadicus Date: Sat, 9 Jun 2018 19:23:28 -0400 Subject: [PATCH 18/18] Get code coverage to 100% --- tests/DeriveHeaderStrategyTest.php | 24 +++++++++++++++++++++ tests/NoHeaderStrategyTest.php | 31 ++++++++++++++++++++++++++++ tests/ProvidedHeaderStrategyTest.php | 30 +++++++++++++++++++++++++++ 3 files changed, 85 insertions(+) diff --git a/tests/DeriveHeaderStrategyTest.php b/tests/DeriveHeaderStrategyTest.php index 8a1553c..fd250bf 100644 --- a/tests/DeriveHeaderStrategyTest.php +++ b/tests/DeriveHeaderStrategyTest.php @@ -50,6 +50,30 @@ public function rowNotIsHeaderRow() $this->assertFalse($strategy->isHeaderRow($fileObject->fgetcsv())); } + /** + * @test + * @covers ::createDataRow + */ + public function createDataRow() + { + $fileObject = $this->getFileObject(); + $strategy = new DeriveHeaderStrategy(); + $strategy->getHeaders($fileObject); + $fileObject->fgetcsv();//skip header line + $this->assertSame( + [ + 'id' => 'bk101', + 'author' => 'Gambardella, Matthew', + 'title' => 'XML Developer\'s Guide', + 'genre' => 'Computer', + 'price' => '44.95', + 'publish_date' => '2000-10-01', + 'description' => 'An in-depth look at creating applications with XML.', + ], + $strategy->createDataRow($fileObject->fgetcsv()) + ); + } + private function getFileObject() : SplFileObject { $fileObject = new SplFileObject(__DIR__ . '/_files/pipe_delimited.txt'); diff --git a/tests/NoHeaderStrategyTest.php b/tests/NoHeaderStrategyTest.php index ef77177..276ae7c 100644 --- a/tests/NoHeaderStrategyTest.php +++ b/tests/NoHeaderStrategyTest.php @@ -40,4 +40,35 @@ public function isHeaderRowAlwaysReturnsFalse() $this->assertFalse($strategy->isHeaderRow($fileObject->fgetcsv())); $this->assertFalse($strategy->isHeaderRow($fileObject->fgetcsv())); } + + /** + * @test + * @covers ::createDataRow + */ + public function createDataRow() + { + $row = [ + 'bk101', + 'Gambardella, Matthew', + 'XML Developer\'s Guide', + 'Computer', + '44.95', + '2000-10-01', + 'An in-depth look at creating applications with XML.', + ]; + $strategy = new NoHeaderStrategy(); + $this->assertSame( + [ + 'bk101', + 'Gambardella, Matthew', + 'XML Developer\'s Guide', + 'Computer', + '44.95', + '2000-10-01', + 'An in-depth look at creating applications with XML.', + ], + $strategy->createDataRow($row) + ); + } + } diff --git a/tests/ProvidedHeaderStrategyTest.php b/tests/ProvidedHeaderStrategyTest.php index 7409e78..9f3e7ea 100644 --- a/tests/ProvidedHeaderStrategyTest.php +++ b/tests/ProvidedHeaderStrategyTest.php @@ -47,6 +47,36 @@ public function rowIsNotHeaderRow() $this->assertFalse($strategy->isHeaderRow($fileObject->fgetcsv())); } + /** + * @test + * @covers ::createDataRow + */ + public function createDataRow() + { + $row = [ + 'bk101', + 'Gambardella, Matthew', + 'XML Developer\'s Guide', + 'Computer', + '44.95', + '2000-10-01', + 'An in-depth look at creating applications with XML.', + ]; + $strategy = $this->getStrategy(); + $this->assertSame( + [ + 'id' => 'bk101', + 'author' => 'Gambardella, Matthew', + 'title' => 'XML Developer\'s Guide', + 'genre' => 'Computer', + 'price' => '44.95', + 'publish_date' => '2000-10-01', + 'description' => 'An in-depth look at creating applications with XML.', + ], + $strategy->createDataRow($row) + ); + } + private function getFileObject() : SplFileObject { $fileObject = new SplFileObject(__DIR__ . '/_files/basic.csv');