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)
+
+
+
+
+
+## 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 @@
+