From 104b6e04b31527b976ab7e28b0749788cb1837e0 Mon Sep 17 00:00:00 2001 From: Jan Pecha Date: Fri, 22 Jun 2018 10:47:22 +0200 Subject: [PATCH] init commit --- .gitignore | 3 + .travis.yml | 28 ++++ Makefile | 15 ++ composer.json | 21 +++ license.md | 26 ++++ readme.md | 42 ++++++ src/CsvIterator.php | 159 ++++++++++++++++++++ src/exceptions.php | 8 + tests/.gitignore | 2 + tests/CsvIterator/basic.phpt | 34 +++++ tests/CsvIterator/csv/basic.csv | 7 + tests/CsvIterator/csv/custom-format.csv | 2 + tests/CsvIterator/csv/header.duplicate.csv | 2 + tests/CsvIterator/csv/header.empty-cell.csv | 2 + tests/CsvIterator/custom-format.phpt | 20 +++ tests/CsvIterator/exceptions.phpt | 36 +++++ tests/bootstrap.php | 11 ++ tests/php-unix.ini | 2 + 18 files changed, 420 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 Makefile create mode 100644 composer.json create mode 100644 license.md create mode 100644 readme.md create mode 100644 src/CsvIterator.php create mode 100644 src/exceptions.php create mode 100644 tests/.gitignore create mode 100644 tests/CsvIterator/basic.phpt create mode 100644 tests/CsvIterator/csv/basic.csv create mode 100644 tests/CsvIterator/csv/custom-format.csv create mode 100644 tests/CsvIterator/csv/header.duplicate.csv create mode 100644 tests/CsvIterator/csv/header.empty-cell.csv create mode 100644 tests/CsvIterator/custom-format.phpt create mode 100644 tests/CsvIterator/exceptions.phpt create mode 100644 tests/bootstrap.php create mode 100644 tests/php-unix.ini diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f9f6cb6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/vendor/ +composer.lock +*.sublime-project diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ec5bcaa --- /dev/null +++ b/.travis.yml @@ -0,0 +1,28 @@ + +language: php +php: + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - 7.1 + - 7.2 + +script: + - vendor/bin/tester tests -s -p php + - php temp/code-checker/src/code-checker.php + +after_failure: + # Print *.actual content + - for i in $(find tests -name \*.actual); do echo "--- $i"; cat $i; echo; echo; done + +before_script: + # Install Nette Tester + - travis_retry composer install --no-interaction --prefer-dist + - travis_retry composer create-project nette/code-checker temp/code-checker ~2 + +sudo: false + +cache: + directories: + - $HOME/.composer/cache diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..43f7b46 --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +tester = vendor/bin/tester +tests_dir = tests/ +coverage_name = $(tests_dir)coverage.html +php_ini = $(tests_dir)php-unix.ini +php_bin = php + +.PHONY: test coverage clean +test: + @$(tester) -p $(php_bin) -c $(php_ini) $(tests_dir) + +coverage: + @$(tester) -p $(php_bin) -c $(php_ini) --coverage $(coverage_name) --coverage-src src/ $(tests_dir) + +clean: + @rm -f $(coverage_name) diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..f418bba --- /dev/null +++ b/composer.json @@ -0,0 +1,21 @@ +{ + "name": "czproject/csv-iterator", + "type": "library", + "description": "Simple reading of CSV files.", + "license": "BSD-3-Clause", + "authors": [ + { + "name": "Jan Pecha", + "homepage": "https://www.janpecha.cz/" + } + ], + "require": { + "php": ">=5.4.0" + }, + "autoload": { + "classmap": ["src/"] + }, + "require-dev": { + "nette/tester": "^1.7" + } +} diff --git a/license.md b/license.md new file mode 100644 index 0000000..23e6b99 --- /dev/null +++ b/license.md @@ -0,0 +1,26 @@ +New BSD License +--------------- + +Copyright © 2018 Jan Pecha (https://www.janpecha.cz/) All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. +* Neither the name of “CzProject“ nor the names of its contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..efc3b56 --- /dev/null +++ b/readme.md @@ -0,0 +1,42 @@ + +# CsvIterator + +Simple reading of CSV files. + +[![Build Status](https://travis-ci.org/czproject/csv-iterator.svg?branch=master)](https://travis-ci.org/czproject/csv-iterator) + +Become a Patron! +Buy me a coffee + + +## Installation + +[Download a latest package](https://github.com/czproject/csv-iterator/releases) or use [Composer](http://getcomposer.org/): + +``` +composer require czproject/csv-iterator +``` + +CsvIterator requires PHP 5.4.0 or later. + + +## Usage + +```csv +id,name +1,Gandalf The White +``` + +```php +$iterator = new CzProject\CsvIterator\CsvIterator('/path/to/file.csv'); + +while (($row = $iterator->fetch()) !== NULL) { + echo $row['id']; // prints '1' + echo $row['name']; // prints 'Gandalf The White' +} +``` + +------------------------------ + +License: [New BSD License](license.md) +
Author: Jan Pecha, https://www.janpecha.cz/ diff --git a/src/CsvIterator.php b/src/CsvIterator.php new file mode 100644 index 0000000..490e5bc --- /dev/null +++ b/src/CsvIterator.php @@ -0,0 +1,159 @@ +file = $file; + } + + + /** + * @param string + * @return self + */ + public function setDelimiter($delimiter) + { + $this->delimiter = $delimiter; + return $this; + } + + + /** + * @param string + * @return self + */ + public function setEnclosure($enclosure) + { + $this->enclosure = $enclosure; + return $this; + } + + + /** + * @param string + * @return self + */ + public function setEscape($escape) + { + $this->escape = $escape; + return $this; + } + + + /** + * @return array|NULL + * @throws CsvIteratorException + */ + public function fetch() + { + if ($this->eof) { + return NULL; + } + + $this->open(); + $data = NULL; + + do { + $data = fgetcsv($this->pointer, 0, $this->delimiter, $this->enclosure, $this->escape); + + if ($data === FALSE || $data === NULL) { + $this->eof = TRUE; + fclose($this->pointer); + return NULL; + } + + if (is_array($data) && count($data) === 1 && $data[0] === NULL) { // empty line + $data = NULL; + } + + } while (!is_array($data) || empty($data)); + + if ($this->header === NULL) { // parse header + $this->header = array(); + + foreach ($data as $i => $value) { + $value = $this->normalizeValue($value); + + if ($value === '') { + throw new CsvIteratorException('Empty header cell at position ' . $i . '.'); + } + + if (isset($this->header[$value])) { + throw new CsvIteratorException('Duplicate header \'' . $value . '\'.'); + } + + $this->header[$value] = $i; + } + + return $this->fetch(); // fetch next row + + } + + // parse data + $row = array(); + + foreach ($this->header as $column => $i) { + $value = NULL; + + if (isset($data[$i])) { + $value = $this->normalizeValue($data[$i]); + } + + $row[$column] = $value !== '' ? $value : NULL; + } + + return $row; + } + + + private function open() + { + if ($this->pointer === NULL) { + $f = @fopen($this->file, 'r'); // @ intentionally + + if ($f === FALSE) { + throw new CsvIteratorException('File \'' . $this->file . '\' not found or access denied.'); + } + + $this->pointer = $f; + } + } + + + private function normalizeValue($value) + { + if ($this->escape !== '' && $this->enclosure !== '') { // fgetcsv() dosn't return unescaped strings, see http://php.net/manual/en/function.fgetcsv.php#119896 + $escapeChar = $this->escape . $this->enclosure; + $value = strtr($value, array( + $escapeChar => $this->enclosure, + )); + } + return trim($value); + } + } diff --git a/src/exceptions.php b/src/exceptions.php new file mode 100644 index 0000000..6b897e8 --- /dev/null +++ b/src/exceptions.php @@ -0,0 +1,8 @@ + '1', + 'name' => 'Gandalf The White', + ), $iterator->fetch()); + + Assert::same(array( + 'id' => '2', + 'name' => 'Harry Potter', + ), $iterator->fetch()); + + Assert::same(array( + 'id' => '3', + 'name' => NULL, + ), $iterator->fetch()); + + Assert::same(array( + 'id' => NULL, + 'name' => NULL, + ), $iterator->fetch()); + + Assert::null($iterator->fetch()); // EOF + Assert::null($iterator->fetch()); // closed file +}); diff --git a/tests/CsvIterator/csv/basic.csv b/tests/CsvIterator/csv/basic.csv new file mode 100644 index 0000000..4e46821 --- /dev/null +++ b/tests/CsvIterator/csv/basic.csv @@ -0,0 +1,7 @@ +id,name +1,Gandalf The White +2,Harry Potter + + +3, +, diff --git a/tests/CsvIterator/csv/custom-format.csv b/tests/CsvIterator/csv/custom-format.csv new file mode 100644 index 0000000..7a3dcca --- /dev/null +++ b/tests/CsvIterator/csv/custom-format.csv @@ -0,0 +1,2 @@ +id;name +1;'Potter \' Harry' diff --git a/tests/CsvIterator/csv/header.duplicate.csv b/tests/CsvIterator/csv/header.duplicate.csv new file mode 100644 index 0000000..5750fb7 --- /dev/null +++ b/tests/CsvIterator/csv/header.duplicate.csv @@ -0,0 +1,2 @@ +id,id +1,Gandalf The White diff --git a/tests/CsvIterator/csv/header.empty-cell.csv b/tests/CsvIterator/csv/header.empty-cell.csv new file mode 100644 index 0000000..8e989aa --- /dev/null +++ b/tests/CsvIterator/csv/header.empty-cell.csv @@ -0,0 +1,2 @@ +id,,name +1,,Gandalf The White diff --git a/tests/CsvIterator/custom-format.phpt b/tests/CsvIterator/custom-format.phpt new file mode 100644 index 0000000..4cfc3ec --- /dev/null +++ b/tests/CsvIterator/custom-format.phpt @@ -0,0 +1,20 @@ +setDelimiter(';'); + $iterator->setEnclosure('\''); + $iterator->setEscape('\\'); + + Assert::same(array( + 'id' => '1', + 'name' => 'Potter \' Harry', + ), $iterator->fetch()); + +}); diff --git a/tests/CsvIterator/exceptions.phpt b/tests/CsvIterator/exceptions.phpt new file mode 100644 index 0000000..b47f0d5 --- /dev/null +++ b/tests/CsvIterator/exceptions.phpt @@ -0,0 +1,36 @@ +fetch(); + + }, 'CzProject\CsvIterator\CsvIteratorException', 'Empty header cell at position 1.'); +}); + + +test(function () { + Assert::exception(function () { + + $iterator = new CsvIterator(__DIR__ . '/csv/header.duplicate.csv'); + $iterator->fetch(); + + }, 'CzProject\CsvIterator\CsvIteratorException', 'Duplicate header \'id\'.'); +}); + + +test(function () { + Assert::exception(function () { + + $iterator = new CsvIterator(__DIR__ . '/csv/not-found.csv'); + $iterator->fetch(); + + }, 'CzProject\CsvIterator\CsvIteratorException', 'File \'' . __DIR__ . '/csv/not-found.csv' . '\' not found or access denied.'); +}); diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..893e8d2 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,11 @@ +