diff --git a/README.md b/README.md index 4a6a22d..ec73637 100644 --- a/README.md +++ b/README.md @@ -7,26 +7,25 @@ The library provides a simple reader and writer for CSV files according to [RFC4180](https://tools.ietf.org/html/rfc4180). The library is licensed under the [MIT](https://github.com/keboola/php-csv/blob/master/LICENSE) license. The library provides -a single `CsvFile` class for both reading and writing CSV files. The class is designed to be **immutable** and minimalistic. +classes `CsvReader` and `CsvWriter` for reading and writing CSV files. The classes are designed to be **immutable** +and minimalistic. ## Usage ### Read CSV ```php -$csvFile = new Keboola\Csv\CsvFile(__DIR__ . '/_data/test-input.csv'); +$csvFile = new Keboola\Csv\CsvReader(__DIR__ . '/_data/test-input.csv'); foreach($csvFile as $row) { var_dump($row); } ``` #### Skip lines -Skip the first two lines: +Skip the first line: ```php -use Keboola\Csv\CsvFile; -$filename = __DIR__ . '/_data/test-input.csv'; -$csvFile = new \Keboola\Csv\CsvFile($fileName, CsvFile::DEFAULT_DELIMITER, CsvFile::DEFAULT_ENCLOSURE, CsvFile::DEFAULT_ENCLOSURE, 2) +$csvFile = new \Keboola\Csv\CsvFile($fileName, CsvFile::DEFAULT_DELIMITER, CsvFile::DEFAULT_ENCLOSURE, CsvFile::DEFAULT_ESCAPED_BY, 1) foreach($csvFile as $row) { var_dump($row); } @@ -36,21 +35,42 @@ foreach($csvFile as $row) { ### Write CSV ```php -$csvFile = new Keboola\Csv\CsvFile(__DIR__ . '/_data/test-output.csv'); -$rows = array( - array( +$csvFile = new Keboola\Csv\CsvWriter(__DIR__ . '/_data/test-output.csv'); +$rows = [ + [ 'col1', 'col2', - ), - array( - 'line without enclosure', 'second column', - ), -); + ], + [ + 'first column', 'second column', + ], +]; foreach ($rows as $row) { $csvFile->writeRow($row); } ``` +### Append to CSV + +```php +$fileName = __DIR__ . '/_data/test-output.csv'; +$file = fopen($fileName, 'a'); +$csvFile = new Keboola\Csv\CsvWriter($file); +$rows = [ + [ + 'col1', 'col2', + ], + [ + 'first column', 'second column', + ], +]; + +foreach ($rows as $row) { + $csvFile->writeRow($row); +} +fclose($file); +``` + ## Installation The library is available as [composer package](https://getcomposer.org/doc/00-intro.md#installation-linux-unix-osx). @@ -69,5 +89,4 @@ composer require keboola/csv require 'vendor/autoload.php'; ``` - Read more in [Composer documentation](http://getcomposer.org/doc/01-basic-usage.md) diff --git a/src/AbstractCsvFile.php b/src/AbstractCsvFile.php new file mode 100644 index 0000000..fb88c2b --- /dev/null +++ b/src/AbstractCsvFile.php @@ -0,0 +1,137 @@ +delimiter; + } + + /** + * @param string $delimiter + * @throws InvalidArgumentException + */ + protected function setDelimiter($delimiter) + { + $this->validateDelimiter($delimiter); + $this->delimiter = $delimiter; + } + + /** + * @param string $delimiter + * @throws InvalidArgumentException + */ + protected function validateDelimiter($delimiter) + { + if (strlen($delimiter) > 1) { + throw new InvalidArgumentException( + "Delimiter must be a single character. " . json_encode($delimiter) . " received", + Exception::INVALID_PARAM + ); + } + + if (strlen($delimiter) == 0) { + throw new InvalidArgumentException( + "Delimiter cannot be empty.", + Exception::INVALID_PARAM + ); + } + } + + /** + * @return string + */ + public function getEnclosure() + { + return $this->enclosure; + } + + /** + * @param string $enclosure + * @return $this + * @throws InvalidArgumentException + */ + protected function setEnclosure($enclosure) + { + $this->validateEnclosure($enclosure); + $this->enclosure = $enclosure; + return $this; + } + + /** + * @param string $enclosure + * @throws InvalidArgumentException + */ + protected function validateEnclosure($enclosure) + { + if (strlen($enclosure) > 1) { + throw new InvalidArgumentException( + "Enclosure must be a single character. " . json_encode($enclosure) . " received", + Exception::INVALID_PARAM + ); + } + } + + public function __destruct() + { + $this->closeFile(); + } + + protected function closeFile() + { + if ($this->fileName && is_resource($this->filePointer)) { + fclose($this->filePointer); + } + } + + /** + * @param string|resource $file + */ + protected function setFile($file) + { + if (is_string($file)) { + $this->openCsvFile($file); + $this->fileName = $file; + } elseif (is_resource($file)) { + $this->filePointer = $file; + } else { + throw new InvalidArgumentException("Invalid file: " . var_export($file, true)); + } + } + + abstract protected function openCsvFile($fileName); + + /** + * @return resource + */ + protected function getFilePointer() + { + return $this->filePointer; + } +} diff --git a/src/CsvFile.php b/src/CsvFile.php deleted file mode 100644 index ee9892e..0000000 --- a/src/CsvFile.php +++ /dev/null @@ -1,465 +0,0 @@ -escapedBy = $escapedBy; - $this->setDelimiter($delimiter); - $this->setEnclosure($enclosure); - $this->setSkipLines($skipLines); - } - - public function __destruct() - { - $this->closeFile(); - } - - /** - * @param integer $skipLines - * @return CsvFile - * @throws InvalidArgumentException - */ - protected function setSkipLines($skipLines) - { - $this->validateSkipLines($skipLines); - $this->skipLines = $skipLines; - return $this; - } - - /** - * @param integer $skipLines - * @throws InvalidArgumentException - */ - protected function validateSkipLines($skipLines) - { - if (!is_int($skipLines) || $skipLines < 0) { - throw new InvalidArgumentException( - "Number of lines to skip must be a positive integer. \"$skipLines\" received.", - Exception::INVALID_PARAM, - null, - Exception::INVALID_PARAM_STR - ); - } - } - - /** - * @param string $delimiter - * @return CsvFile - * @throws InvalidArgumentException - */ - protected function setDelimiter($delimiter) - { - $this->validateDelimiter($delimiter); - $this->delimiter = $delimiter; - return $this; - } - - /** - * @param string $delimiter - * @throws InvalidArgumentException - */ - protected function validateDelimiter($delimiter) - { - if (strlen($delimiter) > 1) { - throw new InvalidArgumentException( - "Delimiter must be a single character. \"$delimiter\" received", - Exception::INVALID_PARAM, - null, - Exception::INVALID_PARAM_STR - ); - } - - if (strlen($delimiter) == 0) { - throw new InvalidArgumentException( - "Delimiter cannot be empty.", - Exception::INVALID_PARAM, - null, - Exception::INVALID_PARAM_STR - ); - } - } - - /** - * @param string $enclosure - * @return $this - * @throws InvalidArgumentException - */ - protected function setEnclosure($enclosure) - { - $this->validateEnclosure($enclosure); - $this->enclosure = $enclosure; - return $this; - } - - /** - * @param string $enclosure - * @throws InvalidArgumentException - */ - protected function validateEnclosure($enclosure) - { - if (strlen($enclosure) > 1) { - throw new InvalidArgumentException( - "Enclosure must be a single character. \"$enclosure\" received", - Exception::INVALID_PARAM, - null, - Exception::INVALID_PARAM_STR - ); - } - } - - /** - * @return string - * @throws Exception - */ - protected function detectLineBreak() - { - rewind($this->getFilePointer()); - $sample = fread($this->getFilePointer(), 10000); - rewind($this->getFilePointer()); - - $possibleLineBreaks = [ - "\r\n", // win - "\r", // mac - "\n", // unix - ]; - - $lineBreaksPositions = []; - foreach ($possibleLineBreaks as $lineBreak) { - $position = strpos($sample, $lineBreak); - if ($position === false) { - continue; - } - $lineBreaksPositions[$lineBreak] = $position; - } - - - asort($lineBreaksPositions); - reset($lineBreaksPositions); - - return empty($lineBreaksPositions) ? "\n" : key($lineBreaksPositions); - } - - /** - * @return array|false|null - * @throws Exception - * @throws InvalidArgumentException - */ - protected function readLine() - { - $this->validateLineBreak(); - - // allow empty enclosure hack - $enclosure = !$this->getEnclosure() ? chr(0) : $this->getEnclosure(); - $escapedBy = !$this->escapedBy ? chr(0) : $this->escapedBy; - return fgetcsv($this->getFilePointer(), null, $this->getDelimiter(), $enclosure, $escapedBy); - } - - /** - * @param string $mode - * @return resource - * @throws Exception - */ - protected function getFilePointer($mode = 'r') - { - if (!is_resource($this->filePointer)) { - $this->openCsvFile($mode); - } - return $this->filePointer; - } - - /** - * @param string $mode - * @throws Exception - */ - protected function openCsvFile($mode) - { - if ($mode == 'r' && !is_file($this->getPathname())) { - throw new Exception( - "Cannot open file {$this->getPathname()}", - Exception::FILE_NOT_EXISTS, - null, - Exception::FILE_NOT_EXISTS_STR - ); - } - $this->filePointer = @fopen($this->getPathname(), $mode); - if (!$this->filePointer) { - throw new Exception( - "Cannot open file {$this->getPathname()} " . error_get_last()['message'], - Exception::FILE_NOT_EXISTS, - null, - Exception::FILE_NOT_EXISTS_STR - ); - } - } - - protected function closeFile() - { - if (is_resource($this->filePointer)) { - fclose($this->filePointer); - } - } - - /** - * @return string - */ - public function getDelimiter() - { - return $this->delimiter; - } - - /** - * @return string - */ - public function getEnclosure() - { - return $this->enclosure; - } - - /** - * @return string - */ - public function getEscapedBy() - { - return $this->escapedBy; - } - - /** - * @return int - */ - public function getColumnsCount() - { - return count($this->getHeader()); - } - - /** - * @return array - */ - public function getHeader() - { - $this->rewind(); - $current = $this->current(); - if (is_array($current)) { - return $current; - } - - return []; - } - - /** - * @param array $row - * @throws Exception - */ - public function writeRow(array $row) - { - $str = $this->rowToStr($row); - $ret = fwrite($this->getFilePointer('w+'), $str); - - /* According to http://php.net/fwrite the fwrite() function - should return false on error. However not writing the full - string (which may occur e.g. when disk is full) is not considered - as an error. Therefore both conditions are necessary. */ - if (($ret === false) || (($ret === 0) && (strlen($str) > 0))) { - throw new Exception( - "Cannot write to CSV file " . $this->getPathname() . - ($ret === false && error_get_last() ? 'Error: ' . error_get_last()['message'] : '') . - ' Return: ' . json_encode($ret) . - ' To write: ' . strlen($str) . ' Written: ' . $ret, - Exception::WRITE_ERROR, - null, - Exception::WRITE_ERROR_STR - ); - } - } - - /** - * @param array $row - * @return string - * @throws Exception - */ - public function rowToStr(array $row) - { - $return = []; - foreach ($row as $column) { - if (!( - is_scalar($column) - || is_null($column) - || ( - is_object($column) - && method_exists($column, '__toString') - ) - )) { - $type = gettype($column); - throw new Exception( - "Cannot write {$type} into a column", - Exception::WRITE_ERROR, - null, - Exception::WRITE_ERROR_STR, - ['column' => $column] - ); - } - - $return[] = $this->getEnclosure() . - str_replace($this->getEnclosure(), str_repeat($this->getEnclosure(), 2), $column) . - $this->getEnclosure(); - } - return implode($this->getDelimiter(), $return) . "\n"; - } - - /** - * @return string - * @throws Exception - */ - public function getLineBreak() - { - if (!$this->lineBreak) { - $this->lineBreak = $this->detectLineBreak(); - } - return $this->lineBreak; - } - - /** - * @return string - * @throws Exception - */ - public function getLineBreakAsText() - { - return trim(json_encode($this->getLineBreak()), '"'); - } - - /** - * @return string - * @throws InvalidArgumentException - */ - public function validateLineBreak() - { - try { - $lineBreak = $this->getLineBreak(); - } catch (Exception $e) { - throw new InvalidArgumentException( - "Failed to detect line break: " . $e->getMessage(), - Exception::INVALID_PARAM, - $e, - Exception::INVALID_PARAM_STR - ); - } - if (in_array($lineBreak, ["\r\n", "\n"])) { - return $lineBreak; - } - - throw new InvalidArgumentException( - "Invalid line break. Please use unix \\n or win \\r\\n line breaks.", - Exception::INVALID_PARAM, - null, - Exception::INVALID_PARAM_STR - ); - } - - /** - * @inheritdoc - */ - public function current() - { - return $this->currentRow; - } - - /** - * @inheritdoc - */ - public function next() - { - $this->currentRow = $this->readLine(); - $this->rowCounter++; - } - - /** - * @inheritdoc - */ - public function key() - { - return $this->rowCounter; - } - - /** - * @inheritdoc - */ - public function valid() - { - return $this->currentRow !== false; - } - - /** - * @inheritdoc - */ - public function rewind() - { - rewind($this->getFilePointer()); - for ($i = 0; $i < $this->skipLines; $i++) { - $this->readLine(); - } - $this->currentRow = $this->readLine(); - $this->rowCounter = 0; - } -} diff --git a/src/CsvReader.php b/src/CsvReader.php new file mode 100644 index 0000000..7920496 --- /dev/null +++ b/src/CsvReader.php @@ -0,0 +1,271 @@ +escapedBy = $escapedBy; + $this->setDelimiter($delimiter); + $this->setEnclosure($enclosure); + $this->setSkipLines($skipLines); + $this->setFile($file); + $this->lineBreak = $this->detectLineBreak(); + rewind($this->filePointer); + $this->header = $this->readLine(); + $this->rewind(); + } + + /** + * @param integer $skipLines + * @return CsvReader + * @throws InvalidArgumentException + */ + protected function setSkipLines($skipLines) + { + $this->validateSkipLines($skipLines); + $this->skipLines = $skipLines; + return $this; + } + + /** + * @param integer $skipLines + * @throws InvalidArgumentException + */ + protected function validateSkipLines($skipLines) + { + if (!is_int($skipLines) || $skipLines < 0) { + throw new InvalidArgumentException( + "Number of lines to skip must be a positive integer. \"$skipLines\" received.", + Exception::INVALID_PARAM + ); + } + } + + /** + * @param $fileName + * @throws Exception + */ + protected function openCsvFile($fileName) + { + if (!is_file($fileName)) { + throw new Exception( + "Cannot open file " . $fileName, + Exception::FILE_NOT_EXISTS + ); + } + $this->filePointer = @fopen($fileName, "r"); + if (!$this->filePointer) { + throw new Exception( + "Cannot open file {$fileName} " . error_get_last()['message'], + Exception::FILE_NOT_EXISTS + ); + } + } + + /** + * @return string + */ + protected function detectLineBreak() + { + rewind($this->getFilePointer()); + $sample = fread($this->getFilePointer(), 10000); + + $possibleLineBreaks = [ + "\r\n", // win + "\r", // mac + "\n", // unix + ]; + + $lineBreaksPositions = []; + foreach ($possibleLineBreaks as $lineBreak) { + $position = strpos($sample, $lineBreak); + if ($position === false) { + continue; + } + $lineBreaksPositions[$lineBreak] = $position; + } + + + asort($lineBreaksPositions); + reset($lineBreaksPositions); + + return empty($lineBreaksPositions) ? "\n" : key($lineBreaksPositions); + } + + /** + * @return array|false|null + * @throws Exception + * @throws InvalidArgumentException + */ + protected function readLine() + { + $this->validateLineBreak(); + + // allow empty enclosure hack + $enclosure = !$this->getEnclosure() ? chr(0) : $this->getEnclosure(); + $escapedBy = !$this->escapedBy ? chr(0) : $this->escapedBy; + return fgetcsv($this->getFilePointer(), null, $this->getDelimiter(), $enclosure, $escapedBy); + } + + /** + * @return string + * @throws InvalidArgumentException + */ + protected function validateLineBreak() + { + try { + $lineBreak = $this->getLineBreak(); + } catch (Exception $e) { + throw new InvalidArgumentException( + "Failed to detect line break: " . $e->getMessage(), + Exception::INVALID_PARAM, + $e + ); + } + if (in_array($lineBreak, ["\r\n", "\n"])) { + return $lineBreak; + } + + throw new InvalidArgumentException( + "Invalid line break. Please use unix \\n or win \\r\\n line breaks.", + Exception::INVALID_PARAM + ); + } + + /** + * @return string + */ + public function getLineBreak() + { + return $this->lineBreak; + } + + /** + * @inheritdoc + */ + public function rewind() + { + rewind($this->getFilePointer()); + for ($i = 0; $i < $this->skipLines; $i++) { + $this->readLine(); + } + $this->currentRow = $this->readLine(); + $this->rowCounter = 0; + } + + /** + * @return string + */ + public function getEscapedBy() + { + return $this->escapedBy; + } + + /** + * @return int + */ + public function getColumnsCount() + { + return count($this->getHeader()); + } + + /** + * @return array + */ + public function getHeader() + { + if ($this->header) { + return $this->header; + } + return []; + } + + /** + * @return string + */ + public function getLineBreakAsText() + { + return trim(json_encode($this->getLineBreak()), '"'); + } + + /** + * @inheritdoc + */ + public function current() + { + return $this->currentRow; + } + + /** + * @inheritdoc + */ + public function next() + { + $this->currentRow = $this->readLine(); + $this->rowCounter++; + } + + /** + * @inheritdoc + */ + public function key() + { + return $this->rowCounter; + } + + /** + * @inheritdoc + */ + public function valid() + { + return $this->currentRow !== false; + } +} diff --git a/src/CsvWriter.php b/src/CsvWriter.php new file mode 100644 index 0000000..b5daea5 --- /dev/null +++ b/src/CsvWriter.php @@ -0,0 +1,129 @@ +setDelimiter($delimiter); + $this->setEnclosure($enclosure); + $this->setLineBreak($lineBreak); + $this->setFile($file); + } + + /** + * @param string $lineBreak + */ + private function setLineBreak($lineBreak) + { + $this->validateLineBreak($lineBreak); + $this->lineBreak = $lineBreak; + } + + /** + * @param string $lineBreak + */ + private function validateLineBreak($lineBreak) + { + $allowedLineBreaks = [ + "\r\n", // win + "\r", // mac + "\n", // unix + ]; + if (!in_array($lineBreak, $allowedLineBreaks)) { + throw new Exception( + "Invalid line break: " . json_encode($lineBreak) . + " allowed line breaks: " . json_encode($allowedLineBreaks), + Exception::INVALID_PARAM + ); + } + } + + /** + * @param string $fileName + * @throws Exception + */ + protected function openCsvFile($fileName) + { + $this->filePointer = @fopen($fileName, 'w'); + if (!$this->filePointer) { + throw new Exception( + "Cannot open file {$fileName} " . error_get_last()['message'], + Exception::FILE_NOT_EXISTS + ); + } + } + + /** + * @param array $row + * @throws Exception + */ + public function writeRow(array $row) + { + $str = $this->rowToStr($row); + $ret = @fwrite($this->getFilePointer(), $str); + + /* According to http://php.net/fwrite the fwrite() function + should return false on error. However not writing the full + string (which may occur e.g. when disk is full) is not considered + as an error. Therefore both conditions are necessary. */ + if (($ret === false) || (($ret === 0) && (strlen($str) > 0))) { + throw new Exception( + "Cannot write to CSV file " . $this->fileName . + ($ret === false && error_get_last() ? 'Error: ' . error_get_last()['message'] : '') . + ' Return: ' . json_encode($ret) . + ' To write: ' . strlen($str) . ' Written: ' . $ret, + Exception::WRITE_ERROR + ); + } + } + + /** + * @param array $row + * @return string + * @throws Exception + */ + public function rowToStr(array $row) + { + $return = []; + foreach ($row as $column) { + if (!( + is_scalar($column) + || is_null($column) + || ( + is_object($column) + && method_exists($column, '__toString') + ) + )) { + throw new Exception( + "Cannot write data into column: " . var_export($column, true), + Exception::WRITE_ERROR + ); + } + + $return[] = $this->getEnclosure() . + str_replace($this->getEnclosure(), str_repeat($this->getEnclosure(), 2), $column) . + $this->getEnclosure(); + } + return implode($this->getDelimiter(), $return) . $this->lineBreak; + } +} diff --git a/src/Exception.php b/src/Exception.php index 096c33a..3a02d45 100644 --- a/src/Exception.php +++ b/src/Exception.php @@ -7,69 +7,4 @@ class Exception extends \Exception const FILE_NOT_EXISTS = 1; const INVALID_PARAM = 2; const WRITE_ERROR = 3; - - const INVALID_PARAM_STR = 'invalidParam'; - const WRITE_ERROR_STR = 'writeError'; - const FILE_NOT_EXISTS_STR = 'fileNotExists'; - - /** - * @var string - */ - protected $stringCode; - - /** - * @var array - */ - protected $contextParams; - - /** - * Exception constructor. - * @param string $message - * @param int $code - * @param \Throwable $previous - * @param string $stringCode - * @param array|null $params - */ - public function __construct($message = "", $code = 0, $previous = null, $stringCode = null, $params = null) - { - $this->setStringCode($stringCode); - $this->setContextParams($params); - parent::__construct($message, $code, $previous); - } - - /** - * @return string - */ - public function getStringCode() - { - return $this->stringCode; - } - - /** - * @param string $stringCode - * @return Exception - */ - public function setStringCode($stringCode) - { - $this->stringCode = (string)$stringCode; - return $this; - } - - /** - * @return array - */ - public function getContextParams() - { - return $this->contextParams; - } - - /** - * @param array|null $contextParams - * @return Exception - */ - public function setContextParams($contextParams) - { - $this->contextParams = (array)$contextParams; - return $this; - } } diff --git a/tests/CsvFileErrorsTest.php b/tests/CsvFileErrorsTest.php deleted file mode 100644 index 2082be4..0000000 --- a/tests/CsvFileErrorsTest.php +++ /dev/null @@ -1,135 +0,0 @@ -getHeader(); - self::fail("Must throw exception."); - } catch (Exception $e) { - self::assertContains('Cannot open file', $e->getMessage()); - self::assertEquals(1, $e->getCode()); - self::assertEquals([], $e->getContextParams()); - self::assertEquals('fileNotExists', $e->getStringCode()); - } - } - - /** - * @dataProvider invalidFilenameProvider - * @param string $filename - * @param string $message - */ - public function testInvalidFileName($filename, $message) - { - $csv = new CsvFile($filename); - self::expectException(Exception::class); - self::expectExceptionMessage($message); - $csv->writeRow(['a', 'b']); - } - - public function invalidFileNameProvider() - { - return [ - ["", 'Filename cannot be empty'], - ["\0", 'fopen() expects parameter 1 to be a valid path, string given'], - ]; - } - - /** - * @dataProvider invalidDelimiterProvider - * @param string $delimiter - * @param string $message - */ - public function testInvalidDelimiter($delimiter, $message) - { - self::expectException(InvalidArgumentException::class); - self::expectExceptionMessage($message); - new CsvFile(__DIR__ . '/data/test-input.csv', $delimiter); - } - - public function invalidDelimiterProvider() - { - return [ - ['aaaa', 'Delimiter must be a single character. "aaaa" received'], - ['ob g', 'Delimiter must be a single character. "ob g" received'], - ['', 'Delimiter cannot be empty.'], - ]; - } - - /** - * @dataProvider invalidEnclosureProvider - * @param string $enclosure - * @param string $message - */ - public function testInvalidEnclosureShouldThrowException($enclosure, $message) - { - self::expectException(InvalidArgumentException::class); - self::expectExceptionMessage($message); - new CsvFile(__DIR__ . '/data/test-input.csv', ",", $enclosure); - } - - public function invalidEnclosureProvider() - { - return [ - ['aaaa', 'Enclosure must be a single character. "aaaa" received'], - ['ob g', 'Enclosure must be a single character. "ob g" received'], - ]; - } - - public function testNonStringWrite() - { - $fileName = __DIR__ . '/data/_out.csv'; - if (file_exists($fileName)) { - unlink($fileName); - } - - $csvFile = new CsvFile($fileName); - $row = [['nested']]; - self::expectException(Exception::class); - self::expectExceptionMessage("Cannot write array into a column"); - $csvFile->writeRow($row); - } - - /** - * @dataProvider invalidSkipLinesProvider - * @param mixed $skipLines - * @param string $message - */ - public function testInvalidSkipLines($skipLines, $message) - { - self::expectException(Exception::class); - self::expectExceptionMessage($message); - new CsvFile( - 'dummy', - CsvFile::DEFAULT_DELIMITER, - CsvFile::DEFAULT_ENCLOSURE, - CsvFile::DEFAULT_ENCLOSURE, - $skipLines - ); - } - - public function invalidSkipLinesProvider() - { - return [ - ['invalid', 'Number of lines to skip must be a positive integer. "invalid" received.'], - [-123, 'Number of lines to skip must be a positive integer. "-123" received.'] - ]; - } - - public function testInvalidNewLines() - { - $csvFile = new CsvFile(__DIR__ . DIRECTORY_SEPARATOR . 'non-existent'); - self::expectException(Exception::class); - self::expectExceptionMessage('Failed to detect line break: Cannot open file'); - $csvFile->next(); - } -} diff --git a/tests/CsvFileTest.php b/tests/CsvFileTest.php deleted file mode 100644 index 050dab3..0000000 --- a/tests/CsvFileTest.php +++ /dev/null @@ -1,410 +0,0 @@ -getBasename()); - self::assertEquals("\"", $csvFile->getEnclosure()); - self::assertEquals("", $csvFile->getEscapedBy()); - self::assertEquals(",", $csvFile->getDelimiter()); - } - - public function testColumnsCount() - { - $csv = new CsvFile(__DIR__ . '/data/test-input.csv'); - - self::assertEquals(9, $csv->getColumnsCount()); - } - - /** - * @dataProvider validCsvFiles - * @param string $fileName - * @param string $delimiter - */ - public function testRead($fileName, $delimiter) - { - $csvFile = new CsvFile(__DIR__ . '/data/' . $fileName, $delimiter, '"'); - - $expected = [ - "id", - "idAccount", - "date", - "totalFollowers", - "followers", - "totalStatuses", - "statuses", - "kloutScore", - "timestamp", - ]; - self::assertEquals($expected, $csvFile->getHeader()); - } - - public function validCsvFiles() - { - return [ - ['test-input.csv', ','], - ['test-input.win.csv', ','], - ['test-input.tabs.csv', "\t"], - ['test-input.tabs.csv', " "], - ]; - } - - public function testParse() - { - $csvFile = new CsvFile(__DIR__ . '/data/escaping.csv', ",", '"'); - - $rows = []; - foreach ($csvFile as $row) { - $rows[] = $row; - } - - $expected = [ - [ - 'col1', 'col2', - ], - [ - 'line without enclosure', 'second column', - ], - [ - 'enclosure " in column', 'hello \\', - ], - [ - 'line with enclosure', 'second column', - ], - [ - 'column with enclosure ", and comma inside text', 'second column enclosure in text "', - ], - [ - "columns with\nnew line", "columns with\ttab", - ], - [ - "Columns with WINDOWS\r\nnew line", "second", - ], - [ - 'column with \n \t \\\\', 'second col', - ], - ]; - - self::assertEquals($expected, $rows); - } - - public function testParseEscapedBy() - { - $csvFile = new CsvFile(__DIR__ . '/data/escapingEscapedBy.csv', ",", '"', '\\'); - - $expected = [ - [ - 'col1', 'col2', - ], - [ - 'line without enclosure', 'second column', - ], - [ - 'enclosure \" in column', 'hello \\\\', - ], - [ - 'line with enclosure', 'second column', - ], - [ - 'column with enclosure \", and comma inside text', 'second column enclosure in text \"', - ], - [ - "columns with\nnew line", "columns with\ttab", - ], - [ - "Columns with WINDOWS\r\nnew line", "second", - ], - [ - 'column with \n \t \\\\', 'second col', - ], - ]; - - self::assertEquals($expected, iterator_to_array($csvFile)); - } - - public function testEmptyHeader() - { - $csvFile = new CsvFile(__DIR__ . '/data/test-input.empty.csv', ',', '"'); - - self::assertEquals([], $csvFile->getHeader()); - } - - public function testInitInvalidFileShouldNotThrowException() - { - try { - new CsvFile(__DIR__ . '/data/dafadfsafd.csv'); - } catch (\Exception $e) { - self::fail('Exception should not be thrown'); - } - } - - /** - * @param string $file - * @param string $lineBreak - * @param string $lineBreakAsText - * @dataProvider validLineBreaksData - */ - public function testLineEndingsDetection($file, $lineBreak, $lineBreakAsText) - { - $csvFile = new CsvFile(__DIR__ . '/data/' . $file); - self::assertEquals($lineBreak, $csvFile->getLineBreak()); - self::assertEquals($lineBreakAsText, $csvFile->getLineBreakAsText()); - } - - public function validLineBreaksData() - { - return [ - ['test-input.csv', "\n", '\n'], - ['test-input.win.csv', "\r\n", '\r\n'], - ['escaping.csv', "\n", '\n'], - ['just-header.csv', "\n", '\n'], // default - ]; - } - - /** - * @expectedException \Keboola\Csv\InvalidArgumentException - * @dataProvider invalidLineBreaksData - * @param string $file - */ - public function testInvalidLineBreak($file) - { - $csvFile = new CsvFile(__DIR__ . '/data/' . $file); - $csvFile->validateLineBreak(); - } - - public function invalidLineBreaksData() - { - return [ - ['test-input.mac.csv'], - ]; - } - - public function testWrite() - { - $fileName = __DIR__ . '/data/_out.csv'; - if (file_exists($fileName)) { - unlink($fileName); - } - - $csvFile = new CsvFile($fileName); - - $rows = [ - [ - 'col1', 'col2', - ], - [ - 'line without enclosure', 'second column', - ], - [ - 'enclosure " in column', 'hello \\', - ], - [ - 'line with enclosure', 'second column', - ], - [ - 'column with enclosure ", and comma inside text', 'second column enclosure in text "', - ], - [ - "columns with\nnew line", "columns with\ttab", - ], - [ - 'column with \n \t \\\\', 'second col', - ] - ]; - - foreach ($rows as $row) { - $csvFile->writeRow($row); - } - $data = file_get_contents($fileName); - self::assertEquals( - implode( - "\n", - [ - '"col1","col2"', - '"line without enclosure","second column"', - '"enclosure "" in column","hello \\"', - '"line with enclosure","second column"', - '"column with enclosure "", and comma inside text","second column enclosure in text """', - "\"columns with\nnew line\",\"columns with\ttab\"", - '"column with \\n \\t \\\\","second col"', - '', - ] - ), - $data - ); - @unlink($fileName); - } - - public function testWriteInvalidObject() - { - $fileName = __DIR__ . '/data/_out.csv'; - if (file_exists($fileName)) { - unlink($fileName); - } - - $csvFile = new CsvFile($fileName); - - $rows = [ - [ - 'col1', 'col2', - ], - [ - '1', new \stdClass(), - ], - ]; - - $csvFile->writeRow($rows[0]); - self::expectException(Exception::class); - self::expectExceptionMessage("Cannot write object into a column"); - $csvFile->writeRow($rows[1]); - @unlink($fileName); - } - - public function testWriteValidObject() - { - $fileName = __DIR__ . '/data/_out.csv'; - if (file_exists($fileName)) { - unlink($fileName); - } - - $csvFile = new CsvFile($fileName); - $rows = [ - [ - 'col1', 'col2', - ], - [ - '1', new StringObject(), - ], - ]; - - $csvFile->writeRow($rows[0]); - $csvFile->writeRow($rows[1]); - $data = file_get_contents($fileName); - self::assertEquals( - implode( - "\n", - [ - '"col1","col2"' , - '"1","me string"', - '', - ] - ), - $data - ); - @unlink($fileName); - } - - public function testIterator() - { - $csvFile = new CsvFile(__DIR__ . '/data/test-input.csv'); - - $expected = [ - "id", - "idAccount", - "date", - "totalFollowers", - "followers", - "totalStatuses", - "statuses", - "kloutScore", - "timestamp", - ]; - - // header line - $csvFile->rewind(); - self::assertEquals($expected, $csvFile->current()); - - // first line - $csvFile->next(); - self::assertTrue($csvFile->valid()); - - // second line - $csvFile->next(); - self::assertTrue($csvFile->valid()); - - // file end - $csvFile->next(); - self::assertFalse($csvFile->valid()); - } - - public function testSkipsHeaders() - { - $fileName = __DIR__ . '/data/simple.csv'; - - $csvFile = new CsvFile( - $fileName, - CsvFile::DEFAULT_DELIMITER, - CsvFile::DEFAULT_ENCLOSURE, - CsvFile::DEFAULT_ENCLOSURE, - 1 - ); - self::assertEquals([ - ['15', '0'], - ['18', '0'], - ['19', '0'], - ], iterator_to_array($csvFile)); - } - - public function testSkipNoLines() - { - $fileName = __DIR__ . '/data/simple.csv'; - - $csvFile = new CsvFile( - $fileName, - CsvFile::DEFAULT_DELIMITER, - CsvFile::DEFAULT_ENCLOSURE, - CsvFile::DEFAULT_ENCLOSURE, - 0 - ); - self::assertEquals([ - ['id', 'isImported'], - ['15', '0'], - ['18', '0'], - ['19', '0'], - ], iterator_to_array($csvFile)); - } - - public function testSkipsMultipleLines() - { - $fileName = __DIR__ . '/data/simple.csv'; - - $csvFile = new CsvFile( - $fileName, - CsvFile::DEFAULT_DELIMITER, - CsvFile::DEFAULT_ENCLOSURE, - CsvFile::DEFAULT_ENCLOSURE, - 3 - ); - self::assertEquals([ - ['19', '0'], - ], iterator_to_array($csvFile)); - } - - public function testSkipsOverflow() - { - $fileName = __DIR__ . '/data/simple.csv'; - - $csvFile = new CsvFile( - $fileName, - CsvFile::DEFAULT_DELIMITER, - CsvFile::DEFAULT_ENCLOSURE, - CsvFile::DEFAULT_ENCLOSURE, - 100 - ); - self::assertEquals([], iterator_to_array($csvFile)); - } -} diff --git a/tests/CsvReadTest.php b/tests/CsvReadTest.php new file mode 100644 index 0000000..b839b28 --- /dev/null +++ b/tests/CsvReadTest.php @@ -0,0 +1,481 @@ +getEnclosure()); + self::assertEquals("", $csvFile->getEscapedBy()); + self::assertEquals(",", $csvFile->getDelimiter()); + } + + public function testColumnsCount() + { + $csv = new CsvReader(__DIR__ . '/data/test-input.csv'); + self::assertEquals(9, $csv->getColumnsCount()); + } + + /** + * @dataProvider validCsvFiles + * @param string $fileName + * @param string $delimiter + */ + public function testRead($fileName, $delimiter) + { + $csvFile = new CsvReader(__DIR__ . '/data/' . $fileName, $delimiter, '"'); + + $expected = [ + "id", + "idAccount", + "date", + "totalFollowers", + "followers", + "totalStatuses", + "statuses", + "kloutScore", + "timestamp", + ]; + self::assertEquals($expected, $csvFile->getHeader()); + } + + public function validCsvFiles() + { + return [ + ['test-input.csv', ','], + ['test-input.win.csv', ','], + ['test-input.tabs.csv', "\t"], + ['test-input.tabs.csv', " "], + ]; + } + + public function testParse() + { + $csvFile = new CsvReader(__DIR__ . '/data/escaping.csv', ",", '"'); + + $rows = []; + foreach ($csvFile as $row) { + $rows[] = $row; + } + + $expected = [ + [ + 'col1', 'col2', + ], + [ + 'line without enclosure', 'second column', + ], + [ + 'enclosure " in column', 'hello \\', + ], + [ + 'line with enclosure', 'second column', + ], + [ + 'column with enclosure ", and comma inside text', 'second column enclosure in text "', + ], + [ + "columns with\nnew line", "columns with\ttab", + ], + [ + "Columns with WINDOWS\r\nnew line", "second", + ], + [ + 'column with \n \t \\\\', 'second col', + ], + ]; + + self::assertEquals($expected, $rows); + } + + public function testParseEscapedBy() + { + $csvFile = new CsvReader(__DIR__ . '/data/escapingEscapedBy.csv', ",", '"', '\\'); + + $expected = [ + [ + 'col1', 'col2', + ], + [ + 'line without enclosure', 'second column', + ], + [ + 'enclosure \" in column', 'hello \\\\', + ], + [ + 'line with enclosure', 'second column', + ], + [ + 'column with enclosure \", and comma inside text', 'second column enclosure in text \"', + ], + [ + "columns with\nnew line", "columns with\ttab", + ], + [ + "Columns with WINDOWS\r\nnew line", "second", + ], + [ + 'column with \n \t \\\\', 'second col', + ], + ]; + + self::assertEquals($expected, iterator_to_array($csvFile)); + } + + public function testEmptyHeader() + { + $csvFile = new CsvReader(__DIR__ . '/data/test-input.empty.csv', ',', '"'); + self::assertEquals([], $csvFile->getHeader()); + } + + public function testInitInvalidFileShouldThrowException() + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Cannot open file'); + new CsvReader(__DIR__ . '/data/dafadfsafd.csv'); + } + + /** + * @param string $file + * @param string $lineBreak + * @param string $lineBreakAsText + * @dataProvider validLineBreaksData + */ + public function testLineEndingsDetection($file, $lineBreak, $lineBreakAsText) + { + $csvFile = new CsvReader(__DIR__ . '/data/' . $file); + self::assertEquals($lineBreak, $csvFile->getLineBreak()); + self::assertEquals($lineBreakAsText, $csvFile->getLineBreakAsText()); + } + + public function validLineBreaksData() + { + return [ + ['test-input.csv', "\n", '\n'], + ['test-input.win.csv', "\r\n", '\r\n'], + ['escaping.csv', "\n", '\n'], + ['just-header.csv', "\n", '\n'], // default + ]; + } + + public function testInvalidLineBreak() + { + self::expectException(InvalidArgumentException::class); + self::expectExceptionMessage('Invalid line break. Please use unix \n or win \r\n line breaks.'); + new CsvReader(__DIR__ . '/data/test-input.mac.csv'); + } + + public function testIterator() + { + $csvFile = new CsvReader(__DIR__ . '/data/test-input.csv'); + + $expected = [ + "id", + "idAccount", + "date", + "totalFollowers", + "followers", + "totalStatuses", + "statuses", + "kloutScore", + "timestamp", + ]; + + // header line + $csvFile->rewind(); + self::assertEquals($expected, $csvFile->current()); + + // first line + $csvFile->next(); + self::assertTrue($csvFile->valid()); + + // second line + $csvFile->next(); + self::assertTrue($csvFile->valid()); + + // file end + $csvFile->next(); + self::assertFalse($csvFile->valid()); + } + + public function testSkipsHeaders() + { + $fileName = __DIR__ . '/data/simple.csv'; + + $csvFile = new CsvReader( + $fileName, + CsvReader::DEFAULT_DELIMITER, + CsvReader::DEFAULT_ENCLOSURE, + CsvReader::DEFAULT_ESCAPED_BY, + 1 + ); + self::assertEquals(['id', 'isImported'], $csvFile->getHeader()); + self::assertEquals([ + ['15', '0'], + ['18', '0'], + ['19', '0'], + ], iterator_to_array($csvFile)); + } + + public function testSkipNoLines() + { + $fileName = __DIR__ . '/data/simple.csv'; + + $csvFile = new CsvReader( + $fileName, + CsvReader::DEFAULT_DELIMITER, + CsvReader::DEFAULT_ENCLOSURE, + CsvReader::DEFAULT_ESCAPED_BY, + 0 + ); + self::assertEquals(['id', 'isImported'], $csvFile->getHeader()); + self::assertEquals([ + ['id', 'isImported'], + ['15', '0'], + ['18', '0'], + ['19', '0'], + ], iterator_to_array($csvFile)); + } + + public function testSkipsMultipleLines() + { + $fileName = __DIR__ . '/data/simple.csv'; + + $csvFile = new CsvReader( + $fileName, + CsvReader::DEFAULT_DELIMITER, + CsvReader::DEFAULT_ENCLOSURE, + CsvReader::DEFAULT_ESCAPED_BY, + 3 + ); + self::assertEquals(['id', 'isImported'], $csvFile->getHeader()); + self::assertEquals([ + ['19', '0'], + ], iterator_to_array($csvFile)); + } + + public function testSkipsOverflow() + { + $fileName = __DIR__ . '/data/simple.csv'; + + $csvFile = new CsvReader( + $fileName, + CsvReader::DEFAULT_DELIMITER, + CsvReader::DEFAULT_ENCLOSURE, + CsvReader::DEFAULT_ESCAPED_BY, + 100 + ); + self::assertEquals(['id', 'isImported'], $csvFile->getHeader()); + self::assertEquals([], iterator_to_array($csvFile)); + } + + public function testException() + { + try { + $csv = new CsvReader(__DIR__ . '/nonexistent.csv'); + $csv->getHeader(); + self::fail("Must throw exception."); + } catch (Exception $e) { + self::assertContains('Cannot open file', $e->getMessage()); + self::assertEquals(1, $e->getCode()); + } + } + + /** + * @dataProvider invalidDelimiterProvider + * @param string $delimiter + * @param string $message + */ + public function testInvalidDelimiter($delimiter, $message) + { + self::expectException(InvalidArgumentException::class); + self::expectExceptionMessage($message); + new CsvReader(__DIR__ . '/data/test-input.csv', $delimiter); + } + + public function invalidDelimiterProvider() + { + return [ + ['aaaa', 'Delimiter must be a single character. "aaaa" received'], + ['🎁', 'Delimiter must be a single character. "\ud83c\udf81" received'], + [",\n", 'Delimiter must be a single character. ",\n" received'], + ['', 'Delimiter cannot be empty.'], + ]; + } + + /** + * @dataProvider invalidEnclosureProvider + * @param string $enclosure + * @param string $message + */ + public function testInvalidEnclosureShouldThrowException($enclosure, $message) + { + self::expectException(InvalidArgumentException::class); + self::expectExceptionMessage($message); + new CsvReader(__DIR__ . '/data/test-input.csv', ",", $enclosure); + } + + public function invalidEnclosureProvider() + { + return [ + ['aaaa', 'Enclosure must be a single character. "aaaa" received'], + ['ob g', 'Enclosure must be a single character. "ob g" received'], + ]; + } + + /** + * @dataProvider invalidSkipLinesProvider + * @param mixed $skipLines + * @param string $message + */ + public function testInvalidSkipLines($skipLines, $message) + { + self::expectException(Exception::class); + self::expectExceptionMessage($message); + new CsvReader( + 'dummy', + CsvReader::DEFAULT_DELIMITER, + CsvReader::DEFAULT_ENCLOSURE, + CsvReader::DEFAULT_ENCLOSURE, + $skipLines + ); + } + + public function invalidSkipLinesProvider() + { + return [ + ['invalid', 'Number of lines to skip must be a positive integer. "invalid" received.'], + [-123, 'Number of lines to skip must be a positive integer. "-123" received.'] + ]; + } + + public function testInvalidNewLines() + { + self::expectException(Exception::class); + self::expectExceptionMessage('Invalid line break. Please use unix \n or win \r\n line breaks.'); + new CsvReader(__DIR__ . DIRECTORY_SEPARATOR . 'data/binary'); + } + + + public function testValidWithoutRewind() + { + $fileName = __DIR__ . '/data/simple.csv'; + + $csvFile = new CsvReader($fileName); + self::assertTrue($csvFile->valid()); + } + + public function testHeaderNoRewindOnGetHeader() + { + $fileName = __DIR__ . '/data/simple.csv'; + + $csvFile = new CsvReader($fileName); + $csvFile->rewind(); + self::assertEquals(['id', 'isImported'], $csvFile->current()); + $csvFile->next(); + self::assertEquals(['15', '0'], $csvFile->current()); + self::assertEquals(['id', 'isImported'], $csvFile->getHeader()); + self::assertEquals(['15', '0'], $csvFile->current()); + } + + public function testLineBreakWithoutRewind() + { + $fileName = __DIR__ . '/data/simple.csv'; + + $csvFile = new CsvReader($fileName); + self::assertEquals("\n", $csvFile->getLineBreak()); + self::assertEquals(['id', 'isImported'], $csvFile->current()); + } + + public function testWriteReadInTheMiddle() + { + $fileName = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('csv-test'); + $writer = new CsvWriter($fileName); + $reader = new CsvReader($fileName); + self::assertEquals([], $reader->getHeader()); + $rows = [ + [ + 'col1', 'col2', + ], + [ + '1', 'first', + ], + [ + '2', 'second', + ], + ]; + + $writer->writeRow($rows[0]); + $reader->next(); + self::assertEquals(false, $reader->current(), "Reader must be at end of file"); + $writer->writeRow($rows[1]); + $writer->writeRow($rows[2]); + $reader->rewind(); + $reader->next(); + self::assertEquals(['1', 'first'], $reader->current()); + $reader->next(); + self::assertEquals(['2', 'second'], $reader->current()); + $data = file_get_contents($fileName); + self::assertEquals( + implode( + "\n", + [ + '"col1","col2"' , + '"1","first"', + '"2","second"', + '', + ] + ), + $data + ); + } + + public function testReadPointer() + { + $fileName = __DIR__ . '/data/simple.csv'; + $file = fopen($fileName, 'r'); + $csvFile = new CsvReader($file); + self::assertEquals(['id', 'isImported'], $csvFile->getHeader()); + self::assertEquals([ + ['id', 'isImported'], + ['15', '0'], + ['18', '0'], + ['19', '0'], + ], iterator_to_array($csvFile)); + // check that the file pointer remains valid + unset($csvFile); + rewind($file); + $data = fread($file, 13); + self::assertEquals('id,isImported', $data); + } + + public function testInvalidPointer() + { + $fileName = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('csv-test'); + $file = fopen($fileName, 'w'); + $csvFile = new CsvReader($file); + self::assertEquals([], $csvFile->getHeader()); + self::assertEquals([], iterator_to_array($csvFile)); + } + + public function testInvalidFile() + { + self::expectException(Exception::class); + self::expectExceptionMessage('Invalid file: array'); + new CsvReader(['bad']); + } +} diff --git a/tests/CsvWriteTest.php b/tests/CsvWriteTest.php new file mode 100644 index 0000000..218fa3c --- /dev/null +++ b/tests/CsvWriteTest.php @@ -0,0 +1,246 @@ +getEnclosure()); + self::assertEquals(',', $csvFile->getDelimiter()); + } + + public function testWrite() + { + $fileName = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('csv-test'); + $csvFile = new CsvWriter($fileName); + $rows = [ + [ + 'col1', 'col2', + ], + [ + 'line without enclosure', 'second column', + ], + [ + 'enclosure " in column', 'hello \\', + ], + [ + 'line with enclosure', 'second column', + ], + [ + 'column with enclosure ", and comma inside text', 'second column enclosure in text "', + ], + [ + "columns with\nnew line", "columns with\ttab", + ], + [ + 'column with \n \t \\\\', 'second col', + ] + ]; + + foreach ($rows as $row) { + $csvFile->writeRow($row); + } + $data = file_get_contents($fileName); + self::assertEquals( + implode( + "\n", + [ + '"col1","col2"', + '"line without enclosure","second column"', + '"enclosure "" in column","hello \\"', + '"line with enclosure","second column"', + '"column with enclosure "", and comma inside text","second column enclosure in text """', + "\"columns with\nnew line\",\"columns with\ttab\"", + '"column with \\n \\t \\\\","second col"', + '', + ] + ), + $data + ); + } + + public function testWriteInvalidObject() + { + $fileName = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('csv-test'); + $csvFile = new CsvWriter($fileName); + + $rows = [ + [ + 'col1', 'col2', + ], + [ + '1', new \stdClass(), + ], + ]; + + $csvFile->writeRow($rows[0]); + self::expectException(Exception::class); + self::expectExceptionMessage("Cannot write data into column: stdClass::"); + $csvFile->writeRow($rows[1]); + } + + public function testWriteValidObject() + { + $fileName = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('csv-test'); + $csvFile = new CsvWriter($fileName); + $rows = [ + [ + 'col1', 'col2', + ], + [ + '1', new StringObject(), + ], + ]; + + $csvFile->writeRow($rows[0]); + $csvFile->writeRow($rows[1]); + $data = file_get_contents($fileName); + self::assertEquals( + implode( + "\n", + [ + '"col1","col2"' , + '"1","me string"', + '', + ] + ), + $data + ); + } + + /** + * @dataProvider invalidFilenameProvider + * @param string $filename + * @param string $message + */ + public function testInvalidFileName($filename, $message) + { + self::expectException(Exception::class); + self::expectExceptionMessage($message); + new CsvWriter($filename); + } + + public function invalidFileNameProvider() + { + return [ + ["", 'Filename cannot be empty'], + ["\0", 'fopen() expects parameter 1 to be a valid path, string given'], + ]; + } + + public function testNonStringWrite() + { + $fileName = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('csv-test'); + $csvFile = new CsvWriter($fileName); + $row = [['nested']]; + self::expectException(Exception::class); + self::expectExceptionMessage("Cannot write data into column: array"); + $csvFile->writeRow($row); + } + + public function testWritePointer() + { + $fileName = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('csv-test'); + $file = fopen($fileName, 'w'); + $csvFile = new CsvWriter($file); + $rows = [['col1', 'col2']]; + $csvFile->writeRow($rows[0]); + + // check that the file pointer remains valid + unset($csvFile); + fwrite($file, 'foo,bar'); + $data = file_get_contents($fileName); + self::assertEquals( + implode( + "\n", + [ + '"col1","col2"' , + 'foo,bar', + ] + ), + $data + ); + } + + public function testInvalidPointer() + { + $fileName = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('csv-test'); + touch($fileName); + $pointer = fopen($fileName, 'r'); + $csvFile = new CsvWriter($pointer); + $rows = [['col1', 'col2']]; + self::expectException(Exception::class); + self::expectExceptionMessage('Cannot write to CSV file Return: 0 To write: 14 Written: 0'); + $csvFile->writeRow($rows[0]); + } + + public function testInvalidPointer2() + { + $fileName = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('csv-test'); + touch($fileName); + $pointer = fopen($fileName, 'r'); + $csvFile = new CsvWriter($pointer); + fclose($pointer); + $rows = [['col1', 'col2']]; + self::expectException(Exception::class); + self::expectExceptionMessage( + 'a valid stream resource Return: false To write: 14 Written: ' + ); + $csvFile->writeRow($rows[0]); + } + + public function testInvalidFile() + { + self::expectException(Exception::class); + self::expectExceptionMessage('Invalid file: array'); + /** @noinspection PhpParamsInspection */ + new CsvWriter(['dummy']); + } + + public function testWriteLineBreak() + { + $fileName = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('csv-test'); + $csvFile = new CsvWriter( + $fileName, + CsvWriter::DEFAULT_DELIMITER, + CsvWriter::DEFAULT_ENCLOSURE, + "\r\n" + ); + $rows = [ + [ + 'col1', 'col2', + ], + [ + 'val1', 'val2', + ], + ]; + + foreach ($rows as $row) { + $csvFile->writeRow($row); + } + $data = file_get_contents($fileName); + self::assertEquals( + implode( + "\r\n", + [ + '"col1","col2"', + '"val1","val2"', + '', + ] + ), + $data + ); + } +} diff --git a/tests/data/binary b/tests/data/binary new file mode 100644 index 0000000..421f1a7 Binary files /dev/null and b/tests/data/binary differ