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/src/DeriveHeaderStrategy.php b/src/DeriveHeaderStrategy.php new file mode 100644 index 0000000..458ef23 --- /dev/null +++ b/src/DeriveHeaderStrategy.php @@ -0,0 +1,40 @@ +headers = $fileObject->fgetcsv() ?? []; + $fileObject->rewind(); + return $this->headers; + } + + 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 new file mode 100644 index 0000000..65a86ed --- /dev/null +++ b/src/HeaderStrategyInterface.php @@ -0,0 +1,14 @@ +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/src/NoHeaderStrategy.php b/src/NoHeaderStrategy.php new file mode 100644 index 0000000..359696b --- /dev/null +++ b/src/NoHeaderStrategy.php @@ -0,0 +1,29 @@ +fgetcsv(); + $headers = array_keys($firstRow); + $fileObject->rewind(); + return $headers; + } + + 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 new file mode 100644 index 0000000..8cb9db9 --- /dev/null +++ b/src/ProvidedHeaderStrategy.php @@ -0,0 +1,36 @@ +headers = $headers; + } + + public function getHeaders(SplFileObject $fileObject) : array + { + return $this->headers; + } + + 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/Reader.php b/src/Reader.php index 9c0ca70..7eb57f5 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -2,211 +2,97 @@ namespace SubjectivePHP\Csv; +use SplFileObject; + /** * Simple class for reading delimited data files */ -class Reader implements \Iterator +class Reader implements \IteratorAggregate { /** - * The column headers. - * - * @var array|null - */ - 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. - * - * @var resource - */ - private $handle; - - /** - * The current file pointer position. - * - * @var integer + * @var SplFileObject */ - private $position = 0; + private $fileObject; /** - * The current row within the csv file. - * - * @var array|false|null + * @var HeaderStrategyInterface */ - private $current = null; + private $headerStrategy; /** * 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 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. - * @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(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' - ); - } - - 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->headers = $headers; - $this->delimiter = $delimiter; - $this->enclosure = $enclosure; - $this->escapeChar = $escapeChar; - $this->handle = fopen((string)$file, 'r'); + $this->fileObject = $this->getFileObject($file, $csvOptions ?? new CsvOptions()); + $this->headerStrategy = $headerStrategy ?? new DeriveHeaderStrategy(); + $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 = 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(); - } - - $this->current = array_combine($this->headers, $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 = fgetcsv($this->handle, 0, $this->delimiter, $this->enclosure, $this->escapeChar); - 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 { - rewind($this->handle); - $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 !feof($this->handle) && $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() - { - if (is_resource($this->handle)) { - fclose($this->handle); - } + public function current() + { + return $this->strategy->createDataRow(parent::current()); + } + }; } } 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()); + } +} diff --git a/tests/DeriveHeaderStrategyTest.php b/tests/DeriveHeaderStrategyTest.php new file mode 100644 index 0000000..fd250bf --- /dev/null +++ b/tests/DeriveHeaderStrategyTest.php @@ -0,0 +1,84 @@ +getFileObject(); + $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(); + $strategy->getHeaders($fileObject); + $this->assertTrue($strategy->isHeaderRow($fileObject->fgetcsv())); + } + + /** + * @test + * @covers ::isHeaderRow + */ + public function rowNotIsHeaderRow() + { + $strategy = new DeriveHeaderStrategy(); + $fileObject = $this->getFileObject(); + $strategy->getHeaders($fileObject); + $fileObject->fgetcsv(); + $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'); + $fileObject->setFlags(SplFileObject::READ_CSV); + $fileObject->setCsvControl('|'); + return $fileObject; + } +} 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); + } +} diff --git a/tests/NoHeaderStrategyTest.php b/tests/NoHeaderStrategyTest.php new file mode 100644 index 0000000..276ae7c --- /dev/null +++ b/tests/NoHeaderStrategyTest.php @@ -0,0 +1,74 @@ +setFlags(SplFileObject::READ_CSV); + $fileObject->setCsvControl(','); + $strategy = new NoHeaderStrategy(); + $this->assertSame( + [0, 1, 2, 3, 4, 5, 6], + $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())); + } + + /** + * @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 new file mode 100644 index 0000000..9f3e7ea --- /dev/null +++ b/tests/ProvidedHeaderStrategyTest.php @@ -0,0 +1,92 @@ +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())); + } + + /** + * @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'); + $fileObject->setFlags(SplFileObject::READ_CSV); + $fileObject->setCsvControl(','); + return $fileObject; + } + + private function getStrategy() : ProvidedHeaderStrategy + { + return new ProvidedHeaderStrategy(self::HEADERS); + } +} diff --git a/tests/ReaderTest.php b/tests/ReaderTest.php index 6c9dcd8..2eb2303 100644 --- a/tests/ReaderTest.php +++ b/tests/ReaderTest.php @@ -1,6 +1,11 @@ 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. @@ -75,161 +62,171 @@ public function basicUsage(Reader $reader) ], ]; - foreach ($reader as $key => $row) { - $this->assertSame($expected[$key], $row); - } + $this->assertSame($expected, array_values(iterator_to_array($reader))); } /** - * Data provider for basic usage test - * - * @return array + * @test + * @covers ::getIterator */ - public function getReaders() + public function readWithCustomHeaders() { - $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 Reader(__DIR__ . '/_files/tab_delimited.txt', $headers, "\t")], + $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); + $this->assertSame($expected, array_values(iterator_to_array($reader))); } /** - * Verify parameter checks for $file in __construct(). - * - * @param mixed $file The file parameter to check. - * * @test - * @covers ::__construct - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage $file must be a string containing a full path to a readable delimited file - * @dataProvider getFiles - * - * @return void + * @covers ::getIterator */ - public function constructInvalidFileParam($file) + public function readNoHeaders() { - $reader = new Reader($file); + $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', new NoHeaderStrategy()); + $this->assertSame($expected, array_values(iterator_to_array($reader))); } /** - * Data provider for constructInvalidFileParam() test. + * Data provider for basic usage test * * @return array */ - public function getFiles() + public function getReaders() { + $headers = ['id', 'author', 'title', 'genre', 'price', 'publish_date', 'description']; return [ - [__DIR__ . '/_files/not_readable.csv'], - [true], - [null], - [__DIR__ . '/_files/doesnotexist.csv'], + [new Reader(__DIR__ . '/_files/basic.csv')], + [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', + new ProvidedHeaderStrategy($headers), + new CsvOptions('|') + ) + ], + [ + new Reader( + __DIR__ . '/_files/tab_delimited.txt', + new ProvidedHeaderStrategy($headers), + new CsvOptions("\t") + ) + ], ]; } /** - * Verify behavior of __construct with an invalid delimiter. - * - * @test - * @covers ::__construct - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage $delimiter must be a single character string - * - * @return void - */ - public function constructInvalidDelimiter() - { - new Reader(__DIR__ . '/_files/basic.csv', null, 'too long'); - } - - /** - * Verify behavior of __construct with an invalid enclosure. + * Verify parameter checks for $file in __construct(). * * @test * @covers ::__construct * @expectedException \InvalidArgumentException - * @expectedExceptionMessage $enclosure must be a single character string + * @expectedExceptionMessage $file must be a string containing a full path to a readable delimited file * * @return void */ - public function constructInvalidEnclosure() + public function constructWithFileThatDoesNotExist() { - new Reader(__DIR__ . '/_files/basic.csv', null, ',', 123); + new Reader(__DIR__ . '/_files/not_found.csv'); } /** - * Verify behavior of __construct with an invalid escapeChar. - * * @test * @covers ::__construct - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage $escapeChar must be a single character string - * - * @return void - */ - public function constructInvalidEscapeChar() - { - new Reader(__DIR__ . '/_files/basic.csv', null, ',', '"', null); - } - - /** - * Verify behaviour of consecutive rewind(). - * - * @test - * @covers ::rewind - * - * @return void */ - 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. @@ -240,10 +237,8 @@ public function emptyFiles(Reader $reader) { $total = 0; - $reader->rewind(); - while ($reader->valid()) { + foreach ($reader as $row) { $total++; - $reader->next(); } $this->assertSame(0, $total); @@ -260,7 +255,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', new ProvidedHeaderStrategy($headers))], ]; } }