From f3e09ddeb9807f1b0de875cb2389b26d704895a0 Mon Sep 17 00:00:00 2001 From: Leonardo Marquine Date: Tue, 31 Oct 2017 15:45:20 -0200 Subject: [PATCH 001/858] Fix typo in documentation --- docs/9.0/interoperability/enclose-field.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/9.0/interoperability/enclose-field.md b/docs/9.0/interoperability/enclose-field.md index 0fc6080b..b5dd4ff7 100644 --- a/docs/9.0/interoperability/enclose-field.md +++ b/docs/9.0/interoperability/enclose-field.md @@ -80,7 +80,7 @@ $filter = stream_filter_append($resource, EncloseField::getFiltername(), STREAM_ $record = array_map(function ($value) use ($sequence) { return $sequence.$value; -}; $record); +}, $record); fputcsv($resource, $record); ~~~ \ No newline at end of file From 75c0cb227e06b72a69a143812ff498146be999c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Csisz=C3=A1r=20Attila?= Date: Sun, 12 Nov 2017 10:12:17 +0100 Subject: [PATCH 002/858] Fix if using a custom set_error_handler() causing error instead of an exception because error_get_last() returns null in this case --- src/Stream.php | 6 +++++- tests/StreamTest.php | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Stream.php b/src/Stream.php index 34552e0c..035ed4ba 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -176,7 +176,11 @@ public static function createFromPath(string $path, string $open_mode = 'r', $co } if (!$resource = @fopen(...$args)) { - throw new Exception(error_get_last()['message']); + throw new Exception( + error_get_last() + ? error_get_last()['message'] + : 'failed to open stream: No such file or directory' + ); } $instance = new static($resource); diff --git a/tests/StreamTest.php b/tests/StreamTest.php index 216866ca..927fab7b 100644 --- a/tests/StreamTest.php +++ b/tests/StreamTest.php @@ -51,6 +51,20 @@ public function testCreateStreamWithWrongResourceType() new Stream(curl_init()); } + /** + * @covers ::createFromPath + */ + public function testCreateStreamFromPath() + { + set_error_handler(function () { + }); + + $this->expectException(Exception::class); + Stream::createFromPath('no/such/file.csv'); + + restore_error_handler(); + } + /** * @covers ::createFromPath * @covers ::current From 4e9765bc42d8143c8f4ad549cc2066cfd784e99d Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Mon, 13 Nov 2017 09:25:55 +0100 Subject: [PATCH 003/858] applying #254 bugfix to all error_get_last usage --- CHANGELOG.md | 20 ++++++++++++++++++++ src/AbstractCsv.php | 1 - src/Stream.php | 8 ++------ tests/StreamTest.php | 22 ++++++++++++++++++++-- 4 files changed, 42 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fa2e840..f5cdd124 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ All Notable changes to `Csv` will be documented in this file + + +## Next - TBD + +### Added + +- Nothing + +### Deprecated + +- Nothing + +### Fixed + +- issue with `error_get_last` usage when using a modified PHP error handler see [#254](https://github.com/thephpleague/csv/issues/254) - fixed by [@csiszarattila](https://github.com/csiszarattila) + +### Removed + +- Nothing + ## 9.1.0 - 2017-10-20 ### Added diff --git a/src/AbstractCsv.php b/src/AbstractCsv.php index 438cbafd..f8d0c896 100644 --- a/src/AbstractCsv.php +++ b/src/AbstractCsv.php @@ -16,7 +16,6 @@ use Generator; use SplFileObject; -use function League\Csv\bom_match; /** * An abstract class to enable CSV document loading. diff --git a/src/Stream.php b/src/Stream.php index 035ed4ba..9c0e5cad 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -176,11 +176,7 @@ public static function createFromPath(string $path, string $open_mode = 'r', $co } if (!$resource = @fopen(...$args)) { - throw new Exception( - error_get_last() - ? error_get_last()['message'] - : 'failed to open stream: No such file or directory' - ); + throw new Exception(error_get_last()['message'] ?? sprintf('`%s`: failed to open stream: No such file or directory', $path)); } $instance = new static($resource); @@ -226,7 +222,7 @@ public function appendFilter(string $filtername, int $read_write, $params = null return; } - throw new Exception(error_get_last()['message']); + throw new Exception(error_get_last()['message'] ?? sprintf('Enable to locate filter `%s`', $filtername)); } /** diff --git a/tests/StreamTest.php b/tests/StreamTest.php index 927fab7b..5e8dd78d 100644 --- a/tests/StreamTest.php +++ b/tests/StreamTest.php @@ -58,9 +58,11 @@ public function testCreateStreamFromPath() { set_error_handler(function () { }); - + error_clear_last(); + $path = 'no/such/file.csv'; $this->expectException(Exception::class); - Stream::createFromPath('no/such/file.csv'); + $this->expectExceptionMessage('`'.$path.'`: failed to open stream: No such file or directory'); + Stream::createFromPath($path); restore_error_handler(); } @@ -183,4 +185,20 @@ public function testCsvControl() $this->expectException(Exception::class); $doc->setCsvControl(...['foo']); } + + /** + * @covers ::appendFilter + */ + public function testAppendStreamFilterThrowsException() + { + set_error_handler(function () { + }); + $filtername = 'foo.bar'; + error_clear_last(); + $this->expectException(Exception::class); + $this->expectExceptionMessage('Enable to locate filter `'.$filtername.'`'); + $stream = Stream::createFromPath('php://temp', 'r+'); + $stream->appendFilter($filtername, STREAM_FILTER_READ); + restore_error_handler(); + } } From 0aa2c95ff0de7b0f0ec2a5e9bc85121c6219ce2a Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Mon, 13 Nov 2017 16:59:42 +0100 Subject: [PATCH 004/858] update Stream Exception message --- CHANGELOG.md | 3 ++- src/Stream.php | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5cdd124..6583a801 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,6 @@ All Notable changes to `Csv` will be documented in this file - ## Next - TBD ### Added @@ -18,6 +17,8 @@ All Notable changes to `Csv` will be documented in this file - issue with `error_get_last` usage when using a modified PHP error handler see [#254](https://github.com/thephpleague/csv/issues/254) - fixed by [@csiszarattila](https://github.com/csiszarattila) +- Removed seekable word from Stream exception messages. + ### Removed - Nothing diff --git a/src/Stream.php b/src/Stream.php index 9c0e5cad..79751390 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -106,11 +106,11 @@ class Stream implements SeekableIterator public function __construct($resource) { if (!is_resource($resource)) { - throw new TypeError(sprintf('Argument passed must be a seekable stream resource, %s given', gettype($resource))); + throw new TypeError(sprintf('Argument passed must be a stream resource, %s given', gettype($resource))); } if ('stream' !== ($type = get_resource_type($resource))) { - throw new TypeError(sprintf('Argument passed must be a seekable stream resource, %s resource given', $type)); + throw new TypeError(sprintf('Argument passed must be a stream resource, %s resource given', $type)); } $this->is_seekable = stream_get_meta_data($resource)['seekable']; From 66118f5c2a7e4da77e743e69f74773c63b73e8f9 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Tue, 28 Nov 2017 09:25:53 +0100 Subject: [PATCH 005/858] prepare 9.1.1 release --- .travis.yml | 8 +++++--- CHANGELOG.md | 3 +-- src/AbstractCsv.php | 2 +- src/ByteSequence.php | 4 +--- src/CannotInsertRecord.php | 2 +- src/CharsetConverter.php | 2 +- src/ColumnConsistency.php | 2 +- src/EncloseField.php | 2 +- src/EscapeFormula.php | 2 +- src/Exception.php | 2 +- src/HTMLConverter.php | 2 +- src/MapIterator.php | 2 +- src/RFC4180Field.php | 2 +- src/Reader.php | 2 +- src/ResultSet.php | 2 +- src/Statement.php | 2 +- src/Stream.php | 6 +++--- src/Writer.php | 2 +- src/XMLConverter.php | 2 +- src/functions.php | 2 +- tests/StreamTest.php | 11 +---------- 21 files changed, 27 insertions(+), 37 deletions(-) diff --git a/.travis.yml b/.travis.yml index 64e2bee0..b393c0a1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,10 +8,12 @@ matrix: env: COLLECT_COVERAGE=true VALIDATE_CODING_STYLE=true IGNORE_PLATFORMS=false - php: 7.1 env: COLLECT_COVERAGE=true VALIDATE_CODING_STYLE=true IGNORE_PLATFORMS=false - - php: master + - php: 7.2 + env: COLLECT_COVERAGE=false VALIDATE_CODING_STYLE=false IGNORE_PLATFORMS=true + - php: nightly env: COLLECT_COVERAGE=false VALIDATE_CODING_STYLE=false IGNORE_PLATFORMS=true allow_failures: - - php: master + - php: nightly fast_finish: true cache: @@ -30,4 +32,4 @@ script: after_script: - if [ "$COLLECT_COVERAGE" == "true" ]; then wget https://scrutinizer-ci.com/ocular.phar && php ocular.phar code-coverage:upload --format=php-clover build/clover.xml; fi - - if [ "$VALIDATE_CODING_STYLE" == "true" ]; then composer phpcs; fi + - if [ "$VALIDATE_CODING_STYLE" == "true" ]; then composer phpcs; fi \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 6583a801..00bc3102 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,7 @@ All Notable changes to `Csv` will be documented in this file - -## Next - TBD +## 9.1.1 - 2017-11-28 ### Added diff --git a/src/AbstractCsv.php b/src/AbstractCsv.php index f8d0c896..12c0d1a0 100644 --- a/src/AbstractCsv.php +++ b/src/AbstractCsv.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.0 +* @version 9.1.1 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/ByteSequence.php b/src/ByteSequence.php index 32fe76ae..c64db73a 100644 --- a/src/ByteSequence.php +++ b/src/ByteSequence.php @@ -4,14 +4,12 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.0 +* @version 9.1.1 * @package League.csv * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -declare(strict_types=1); - namespace League\Csv; /** diff --git a/src/CannotInsertRecord.php b/src/CannotInsertRecord.php index 51bac901..b0f2da54 100644 --- a/src/CannotInsertRecord.php +++ b/src/CannotInsertRecord.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.0 +* @version 9.1.1 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/CharsetConverter.php b/src/CharsetConverter.php index eb7b3245..f760cb6b 100644 --- a/src/CharsetConverter.php +++ b/src/CharsetConverter.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.0 +* @version 9.1.1 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/ColumnConsistency.php b/src/ColumnConsistency.php index e5029ce9..86314a4d 100644 --- a/src/ColumnConsistency.php +++ b/src/ColumnConsistency.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.0 +* @version 9.1.1 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/EncloseField.php b/src/EncloseField.php index ef3a089f..76225d11 100644 --- a/src/EncloseField.php +++ b/src/EncloseField.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.0 +* @version 9.1.1 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/EscapeFormula.php b/src/EscapeFormula.php index ba3552ab..96e77d7b 100644 --- a/src/EscapeFormula.php +++ b/src/EscapeFormula.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.0 +* @version 9.1.1 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/Exception.php b/src/Exception.php index 0a199058..4fb75c0c 100644 --- a/src/Exception.php +++ b/src/Exception.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.0 +* @version 9.1.1 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/HTMLConverter.php b/src/HTMLConverter.php index e19ba1d8..e30e8b58 100644 --- a/src/HTMLConverter.php +++ b/src/HTMLConverter.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.0 +* @version 9.1.1 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/MapIterator.php b/src/MapIterator.php index da50bcbe..0cdb47cb 100644 --- a/src/MapIterator.php +++ b/src/MapIterator.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.0 +* @version 9.1.1 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/RFC4180Field.php b/src/RFC4180Field.php index 2086de06..b1e1f077 100644 --- a/src/RFC4180Field.php +++ b/src/RFC4180Field.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.0 +* @version 9.1.1 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/Reader.php b/src/Reader.php index 29cf3711..69b0ddbc 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.0 +* @version 9.1.1 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/ResultSet.php b/src/ResultSet.php index ed882ce3..d9c302bb 100644 --- a/src/ResultSet.php +++ b/src/ResultSet.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.0 +* @version 9.1.1 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/Statement.php b/src/Statement.php index 9ab2b2a4..45a12c8f 100644 --- a/src/Statement.php +++ b/src/Statement.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.0 +* @version 9.1.1 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/Stream.php b/src/Stream.php index 79751390..2c5cbe1f 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.0 +* @version 9.1.1 * @package League.csv * * For the full copyright and license information, please view the LICENSE @@ -176,7 +176,7 @@ public static function createFromPath(string $path, string $open_mode = 'r', $co } if (!$resource = @fopen(...$args)) { - throw new Exception(error_get_last()['message'] ?? sprintf('`%s`: failed to open stream: No such file or directory', $path)); + throw new Exception(sprintf('`%s`: failed to open stream: No such file or directory', $path)); } $instance = new static($resource); @@ -222,7 +222,7 @@ public function appendFilter(string $filtername, int $read_write, $params = null return; } - throw new Exception(error_get_last()['message'] ?? sprintf('Enable to locate filter `%s`', $filtername)); + throw new Exception(sprintf('unable to locate filter `%s`', $filtername)); } /** diff --git a/src/Writer.php b/src/Writer.php index 2fafae7c..909b184f 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.0 +* @version 9.1.1 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/XMLConverter.php b/src/XMLConverter.php index 2d25dc6e..63c1bdba 100644 --- a/src/XMLConverter.php +++ b/src/XMLConverter.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.0 +* @version 9.1.1 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/functions.php b/src/functions.php index ecebfeaf..7b3910b2 100644 --- a/src/functions.php +++ b/src/functions.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.0 +* @version 9.1.1 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/tests/StreamTest.php b/tests/StreamTest.php index 5e8dd78d..59d54027 100644 --- a/tests/StreamTest.php +++ b/tests/StreamTest.php @@ -56,15 +56,10 @@ public function testCreateStreamWithWrongResourceType() */ public function testCreateStreamFromPath() { - set_error_handler(function () { - }); - error_clear_last(); $path = 'no/such/file.csv'; $this->expectException(Exception::class); $this->expectExceptionMessage('`'.$path.'`: failed to open stream: No such file or directory'); Stream::createFromPath($path); - - restore_error_handler(); } /** @@ -191,14 +186,10 @@ public function testCsvControl() */ public function testAppendStreamFilterThrowsException() { - set_error_handler(function () { - }); $filtername = 'foo.bar'; - error_clear_last(); $this->expectException(Exception::class); - $this->expectExceptionMessage('Enable to locate filter `'.$filtername.'`'); + $this->expectExceptionMessage('unable to locate filter `'.$filtername.'`'); $stream = Stream::createFromPath('php://temp', 'r+'); $stream->appendFilter($filtername, STREAM_FILTER_READ); - restore_error_handler(); } } From 7d530c4b91e2f419017d71da8483f648854ac58d Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Mon, 5 Feb 2018 08:50:02 +0100 Subject: [PATCH 006/858] improve PHP7.0 polyfill --- CHANGELOG.md | 18 +++++ README.md | 2 +- src/AbstractCsv.php | 2 +- src/ByteSequence.php | 3 +- src/CannotInsertRecord.php | 2 +- src/CharsetConverter.php | 2 +- src/ColumnConsistency.php | 2 +- src/EncloseField.php | 2 +- src/EscapeFormula.php | 2 +- src/Exception.php | 2 +- src/HTMLConverter.php | 2 +- src/MapIterator.php | 2 +- src/RFC4180Field.php | 2 +- src/Reader.php | 2 +- src/ResultSet.php | 2 +- src/Statement.php | 2 +- src/Stream.php | 2 +- src/Writer.php | 2 +- src/XMLConverter.php | 2 +- src/functions.php | 147 ++++++++++++++++++++----------------- 20 files changed, 116 insertions(+), 86 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00bc3102..79556ee0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ All Notable changes to `Csv` will be documented in this file +## Next - TBD + +### Added + +- Nothing + +### Deprecated + +- Nothing + +### Fixed + +- `is_iterable` polyfill for PHP7.0 + +### Removed + +- Nothing + ## 9.1.1 - 2017-11-28 ### Added diff --git a/README.md b/README.md index 77f195fc..d29b75da 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Highlights Documentation ------- -Full documentation can be found at [csv.thephpleague.com](http://csv.thephpleague.com). +Full documentation can be found at [csv.thephpleague.com](https://csv.thephpleague.com). System Requirements ------- diff --git a/src/AbstractCsv.php b/src/AbstractCsv.php index 12c0d1a0..6c957152 100644 --- a/src/AbstractCsv.php +++ b/src/AbstractCsv.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.1 +* @version 9.1.2 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/ByteSequence.php b/src/ByteSequence.php index c64db73a..695f3acf 100644 --- a/src/ByteSequence.php +++ b/src/ByteSequence.php @@ -4,12 +4,13 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.1 +* @version 9.1.2 * @package League.csv * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ + namespace League\Csv; /** diff --git a/src/CannotInsertRecord.php b/src/CannotInsertRecord.php index b0f2da54..a85922fd 100644 --- a/src/CannotInsertRecord.php +++ b/src/CannotInsertRecord.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.1 +* @version 9.1.2 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/CharsetConverter.php b/src/CharsetConverter.php index f760cb6b..240b894d 100644 --- a/src/CharsetConverter.php +++ b/src/CharsetConverter.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.1 +* @version 9.1.2 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/ColumnConsistency.php b/src/ColumnConsistency.php index 86314a4d..86af082e 100644 --- a/src/ColumnConsistency.php +++ b/src/ColumnConsistency.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.1 +* @version 9.1.2 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/EncloseField.php b/src/EncloseField.php index 76225d11..90631233 100644 --- a/src/EncloseField.php +++ b/src/EncloseField.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.1 +* @version 9.1.2 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/EscapeFormula.php b/src/EscapeFormula.php index 96e77d7b..714de111 100644 --- a/src/EscapeFormula.php +++ b/src/EscapeFormula.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.1 +* @version 9.1.2 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/Exception.php b/src/Exception.php index 4fb75c0c..1eeb75f7 100644 --- a/src/Exception.php +++ b/src/Exception.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.1 +* @version 9.1.2 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/HTMLConverter.php b/src/HTMLConverter.php index e30e8b58..687dc113 100644 --- a/src/HTMLConverter.php +++ b/src/HTMLConverter.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.1 +* @version 9.1.2 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/MapIterator.php b/src/MapIterator.php index 0cdb47cb..c4504fe2 100644 --- a/src/MapIterator.php +++ b/src/MapIterator.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.1 +* @version 9.1.2 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/RFC4180Field.php b/src/RFC4180Field.php index b1e1f077..039e5d96 100644 --- a/src/RFC4180Field.php +++ b/src/RFC4180Field.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.1 +* @version 9.1.2 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/Reader.php b/src/Reader.php index 69b0ddbc..f4f66dd3 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.1 +* @version 9.1.2 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/ResultSet.php b/src/ResultSet.php index d9c302bb..679048d1 100644 --- a/src/ResultSet.php +++ b/src/ResultSet.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.1 +* @version 9.1.2 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/Statement.php b/src/Statement.php index 45a12c8f..44046ba7 100644 --- a/src/Statement.php +++ b/src/Statement.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.1 +* @version 9.1.2 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/Stream.php b/src/Stream.php index 2c5cbe1f..28e3d441 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.1 +* @version 9.1.2 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/Writer.php b/src/Writer.php index 909b184f..6d9fe70f 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.1 +* @version 9.1.2 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/XMLConverter.php b/src/XMLConverter.php index 63c1bdba..76363c6b 100644 --- a/src/XMLConverter.php +++ b/src/XMLConverter.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.1 +* @version 9.1.2 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/functions.php b/src/functions.php index 7b3910b2..63a6d7b1 100644 --- a/src/functions.php +++ b/src/functions.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.1 +* @version 9.1.2 * @package League.csv * * For the full copyright and license information, please view the LICENSE @@ -12,71 +12,70 @@ */ declare(strict_types=1); -namespace League\Csv; +namespace League\Csv { -use ReflectionClass; -use Traversable; + use ReflectionClass; + use Traversable; -/** - * Returns the BOM sequence found at the start of the string - * - * If no valid BOM sequence is found an empty string is returned - * - * @param string $str - * - * @return string - */ -function bom_match(string $str): string -{ - static $list; + /** + * Returns the BOM sequence found at the start of the string + * + * If no valid BOM sequence is found an empty string is returned + * + * @param string $str + * + * @return string + */ + function bom_match(string $str): string + { + static $list; - $list = $list ?? (new ReflectionClass(ByteSequence::class))->getConstants(); + $list = $list ?? (new ReflectionClass(ByteSequence::class))->getConstants(); - foreach ($list as $sequence) { - if (0 === strpos($str, $sequence)) { - return $sequence; + foreach ($list as $sequence) { + if (0 === strpos($str, $sequence)) { + return $sequence; + } } - } - return ''; -} + return ''; + } -/** - * Detect Delimiters usage in a {@link Reader} object - * - * Returns a associative array where each key represents - * a submitted delimiter and each value the number CSV fields found - * when processing at most $limit CSV records with the given delimiter - * - * @param Reader $csv the CSV object - * @param string[] $delimiters list of delimiters to consider - * @param int $limit Detection is made using up to $limit records - * - * @return int[] - */ -function delimiter_detect(Reader $csv, array $delimiters, int $limit = 1): array -{ - $found = array_unique(array_filter($delimiters, function (string $value): bool { - return 1 == strlen($value); - })); - $stmt = (new Statement())->limit($limit)->where(function (array $record): bool { - return count($record) > 1; - }); - $reducer = function (array $result, string $delimiter) use ($csv, $stmt): array { - $result[$delimiter] = count(iterator_to_array($stmt->process($csv->setDelimiter($delimiter)), false), COUNT_RECURSIVE); + /** + * Detect Delimiters usage in a {@link Reader} object + * + * Returns a associative array where each key represents + * a submitted delimiter and each value the number CSV fields found + * when processing at most $limit CSV records with the given delimiter + * + * @param Reader $csv the CSV object + * @param string[] $delimiters list of delimiters to consider + * @param int $limit Detection is made using up to $limit records + * + * @return int[] + */ + function delimiter_detect(Reader $csv, array $delimiters, int $limit = 1): array + { + $found = array_unique(array_filter($delimiters, function (string $value): bool { + return 1 == strlen($value); + })); + $stmt = (new Statement())->limit($limit)->where(function (array $record): bool { + return count($record) > 1; + }); + $reducer = function (array $result, string $delimiter) use ($csv, $stmt): array { + $result[$delimiter] = count(iterator_to_array($stmt->process($csv->setDelimiter($delimiter)), false), COUNT_RECURSIVE); - return $result; - }; - $delimiter = $csv->getDelimiter(); - $header_offset = $csv->getHeaderOffset(); - $csv->setHeaderOffset(null); - $stats = array_reduce($found, $reducer, array_fill_keys($delimiters, 0)); - $csv->setHeaderOffset($header_offset)->setDelimiter($delimiter); + return $result; + }; + $delimiter = $csv->getDelimiter(); + $header_offset = $csv->getHeaderOffset(); + $csv->setHeaderOffset(null); + $stats = array_reduce($found, $reducer, array_fill_keys($delimiters, 0)); + $csv->setHeaderOffset($header_offset)->setDelimiter($delimiter); - return $stats; -} + return $stats; + } -if (!function_exists('\is_iterable')) { /** * Tell whether the content of the variable is iterable * @@ -90,18 +89,30 @@ function is_iterable($iterable): bool { return is_array($iterable) || $iterable instanceof Traversable; } + + /** + * Tell whether the content of the variable is an int or null + * + * @see https://wiki.php.net/rfc/nullable_types + * + * @param mixed $value + * + * @return bool + */ + function is_nullable_int($value): bool + { + return null === $value || is_int($value); + } } -/** - * Tell whether the content of the variable is an int or null - * - * @see https://wiki.php.net/rfc/nullable_types - * - * @param mixed $value - * - * @return bool - */ -function is_nullable_int($value): bool -{ - return null === $value || is_int($value); +namespace { + + use League\Csv; + + if (PHP_VERSION_ID < 70100 && !function_exists('\is_iterable')) { + function is_iterable($iterable) + { + return Csv\is_iterable($iterable); + } + } } From 9050afb67bdbcb70f1738b6d0e81055acb61caa0 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Mon, 5 Feb 2018 11:59:26 +0100 Subject: [PATCH 007/858] bugfix issue #279 --- CHANGELOG.md | 1 + src/Reader.php | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79556ee0..f8516f9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ All Notable changes to `Csv` will be documented in this file ### Fixed - `is_iterable` polyfill for PHP7.0 +- internal `Reader::setHeader` no longer throws exception because of a bug in PHP7.2+ [issue #279](https://github.com/thephpleague/csv/issues/213) ### Removed diff --git a/src/Reader.php b/src/Reader.php index f4f66dd3..11fa7727 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.2 +* @version 9.1.1 * @package League.csv * * For the full copyright and license information, please view the LICENSE @@ -20,6 +20,7 @@ use Iterator; use IteratorAggregate; use JsonSerializable; +use LimitIterator; use SplFileObject; use TypeError; @@ -116,8 +117,9 @@ protected function setHeader(int $offset): array { $this->document->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY); $this->document->setCsvControl($this->delimiter, $this->enclosure, $this->escape); - $this->document->seek($offset); - if (empty($header = $this->document->current())) { + $iterator = iterator_to_array(new LimitIterator($this->document, $offset, 1), false); + $header = $iterator[0] ?? []; + if (empty($header)) { throw new Exception(sprintf('The header record does not exist or is empty at offset: `%s`', $offset)); } From 764589836516f5cd91281dedcda21c3648a99b75 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Mon, 5 Feb 2018 13:00:17 +0100 Subject: [PATCH 008/858] improve iterable test --- src/Reader.php | 6 ++---- tests/CsvTest.php | 13 ++++++------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/Reader.php b/src/Reader.php index 11fa7727..69b0ddbc 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -20,7 +20,6 @@ use Iterator; use IteratorAggregate; use JsonSerializable; -use LimitIterator; use SplFileObject; use TypeError; @@ -117,9 +116,8 @@ protected function setHeader(int $offset): array { $this->document->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY); $this->document->setCsvControl($this->delimiter, $this->enclosure, $this->escape); - $iterator = iterator_to_array(new LimitIterator($this->document, $offset, 1), false); - $header = $iterator[0] ?? []; - if (empty($header)) { + $this->document->seek($offset); + if (empty($header = $this->document->current())) { throw new Exception(sprintf('The header record does not exist or is empty at offset: `%s`', $offset)); } diff --git a/tests/CsvTest.php b/tests/CsvTest.php index 3cb8e58a..a85f4070 100644 --- a/tests/CsvTest.php +++ b/tests/CsvTest.php @@ -8,7 +8,6 @@ use PHPUnit\Framework\TestCase; use SplTempFileObject; use TypeError; -use function League\Csv\is_iterable; /** * @group csv @@ -385,13 +384,13 @@ public function testIsIterablePolyFill() $this->markTestSkipped('Polyfill for PHP7.0'); } - $this->assertTrue(is_iterable(['foo'])); - $this->assertTrue(is_iterable(Reader::createFromString(''))); - $this->assertTrue(is_iterable((function () { + $this->assertTrue(\is_iterable(['foo'])); + $this->assertTrue(\is_iterable(Reader::createFromString(''))); + $this->assertTrue(\is_iterable((function () { yield 1; })())); - $this->assertFalse(is_iterable(1)); - $this->assertFalse(is_iterable((object) ['foo'])); - $this->assertFalse(is_iterable(Writer::createFromString(''))); + $this->assertFalse(\is_iterable(1)); + $this->assertFalse(\is_iterable((object) ['foo'])); + $this->assertFalse(\is_iterable(Writer::createFromString(''))); } } From 92f7d62a42473dc911294b8a3ab1be7bf21ad212 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Mon, 5 Feb 2018 13:55:46 +0100 Subject: [PATCH 009/858] work around while waiting for PHP fix for #279 --- src/Reader.php | 17 +++++++++++++++-- tests/ReaderTest.php | 27 ++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/Reader.php b/src/Reader.php index 69b0ddbc..b483a703 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -114,10 +114,23 @@ public function getHeader(): array */ protected function setHeader(int $offset): array { + $header = []; $this->document->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY); $this->document->setCsvControl($this->delimiter, $this->enclosure, $this->escape); - $this->document->seek($offset); - if (empty($header = $this->document->current())) { + if ($this->document instanceof Stream || PHP_VERSION_ID < 70200) { + $this->document->seek($offset); + $header = $this->document->current(); + } else { + $stream->rewind(); + while ($offset !== $stream->key() && $stream->valid()) { + $stream->current(); + $stream->next(); + } + + $header = $stream->current(); + } + + if (empty($header)) { throw new Exception(sprintf('The header record does not exist or is empty at offset: `%s`', $offset)); } diff --git a/tests/ReaderTest.php b/tests/ReaderTest.php index bfbb8406..44c2f206 100644 --- a/tests/ReaderTest.php +++ b/tests/ReaderTest.php @@ -291,12 +291,37 @@ public function appliedFlagsProvider() /** * @covers ::setHeader */ - public function testGetHeaderThrowsException() + public function testGetHeaderThrowsExceptionWithNegativeOffset() + { + $this->expectException(Exception::class); + $this->csv->setHeaderOffset(-3)->getRecords(); + } + + /** + * @covers ::setHeader + */ + public function testGetHeaderThrowsExceptionWithSplFileObject() { $this->expectException(Exception::class); $this->csv->setHeaderOffset(23)->getRecords(); } + /** + * @covers ::setHeader + */ + public function testGetHeaderThrowsExceptionWithStreamObject() + { + $this->expectException(Exception::class); + + $tmp = fopen('php://temp', 'r+'); + foreach ($this->expected as $row) { + fputcsv($tmp, $row); + } + + $csv = Reader::createFromStream($tmp); + $csv->setHeaderOffset(23)->getRecords(); + } + /** * @covers ::setHeaderOffset * @covers \League\Csv\is_nullable_int From 9fe7aebe14350ad6ccedac59d3ac3a55715a0c8f Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Mon, 5 Feb 2018 14:08:04 +0100 Subject: [PATCH 010/858] improve patch --- src/Reader.php | 44 ++++++++++++++++++++++++++++---------------- tests/ReaderTest.php | 3 +++ 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/src/Reader.php b/src/Reader.php index b483a703..59a0baf4 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -114,22 +114,7 @@ public function getHeader(): array */ protected function setHeader(int $offset): array { - $header = []; - $this->document->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY); - $this->document->setCsvControl($this->delimiter, $this->enclosure, $this->escape); - if ($this->document instanceof Stream || PHP_VERSION_ID < 70200) { - $this->document->seek($offset); - $header = $this->document->current(); - } else { - $stream->rewind(); - while ($offset !== $stream->key() && $stream->valid()) { - $stream->current(); - $stream->next(); - } - - $header = $stream->current(); - } - + $header = $this->seekRow($this->document, $offset); if (empty($header)) { throw new Exception(sprintf('The header record does not exist or is empty at offset: `%s`', $offset)); } @@ -141,6 +126,33 @@ protected function setHeader(int $offset): array return $header; } + /** + * Returns the row at a given offset + * + * @param Stream|SplFileObject $stream + * @param int $offset + * + * @return false|array + */ + protected function seekRow($stream, int $offset) + { + $stream->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY); + $stream->setCsvControl($this->delimiter, $this->enclosure, $this->escape); + if ($stream instanceof Stream || PHP_VERSION_ID < 70200) { + $stream->seek($offset); + + return $stream->current(); + } + + $stream->rewind(); + while ($offset !== $stream->key() && $stream->valid()) { + $stream->current(); + $stream->next(); + } + + return $stream->current(); + } + /** * Strip the BOM sequence from a record * diff --git a/tests/ReaderTest.php b/tests/ReaderTest.php index 44c2f206..84987926 100644 --- a/tests/ReaderTest.php +++ b/tests/ReaderTest.php @@ -290,6 +290,7 @@ public function appliedFlagsProvider() /** * @covers ::setHeader + * @covers ::seekRow */ public function testGetHeaderThrowsExceptionWithNegativeOffset() { @@ -299,6 +300,7 @@ public function testGetHeaderThrowsExceptionWithNegativeOffset() /** * @covers ::setHeader + * @covers ::seekRow */ public function testGetHeaderThrowsExceptionWithSplFileObject() { @@ -308,6 +310,7 @@ public function testGetHeaderThrowsExceptionWithSplFileObject() /** * @covers ::setHeader + * @covers ::seekRow */ public function testGetHeaderThrowsExceptionWithStreamObject() { From 2881536465bf607f934104ea6300265655fffcd4 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Mon, 5 Feb 2018 14:40:31 +0100 Subject: [PATCH 011/858] improve patch for SplFileObject::seek bug #279 --- src/Reader.php | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Reader.php b/src/Reader.php index 59a0baf4..870d6806 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -114,7 +114,7 @@ public function getHeader(): array */ protected function setHeader(int $offset): array { - $header = $this->seekRow($this->document, $offset); + $header = $this->seekRow($offset); if (empty($header)) { throw new Exception(sprintf('The header record does not exist or is empty at offset: `%s`', $offset)); } @@ -129,28 +129,28 @@ protected function setHeader(int $offset): array /** * Returns the row at a given offset * - * @param Stream|SplFileObject $stream - * @param int $offset + * @param int $offset * - * @return false|array + * @return mixed */ - protected function seekRow($stream, int $offset) + protected function seekRow(int $offset) { - $stream->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY); - $stream->setCsvControl($this->delimiter, $this->enclosure, $this->escape); - if ($stream instanceof Stream || PHP_VERSION_ID < 70200) { - $stream->seek($offset); + $this->document->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY); + $this->document->setCsvControl($this->delimiter, $this->enclosure, $this->escape); + $this->document->rewind(); + + if ($this->document instanceof Stream || PHP_VERSION_ID < 70200) { + $this->document->seek($offset); - return $stream->current(); + return $this->document->current(); } - $stream->rewind(); - while ($offset !== $stream->key() && $stream->valid()) { - $stream->current(); - $stream->next(); + //Workaround for SplFileObject::seek bug in PHP7.2+ see https://bugs.php.net/bug.php?id=75917 + while ($offset !== $this->document->key() && $this->document->valid()) { + $this->document->next(); } - return $stream->current(); + return $this->document->current(); } /** From 6dba48d217a024d6e78b455694e7f05ba739a9a0 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Mon, 5 Feb 2018 14:49:51 +0100 Subject: [PATCH 012/858] improve patch #279 --- src/Reader.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Reader.php b/src/Reader.php index 870d6806..8881664e 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -139,16 +139,16 @@ protected function seekRow(int $offset) $this->document->setCsvControl($this->delimiter, $this->enclosure, $this->escape); $this->document->rewind(); - if ($this->document instanceof Stream || PHP_VERSION_ID < 70200) { - $this->document->seek($offset); + //Workaround for SplFileObject::seek bug in PHP7.2+ see https://bugs.php.net/bug.php?id=75917 + if (PHP_VERSION_ID >= 70200 && !$this->document instanceof Stream) { + while ($offset !== $this->document->key() && $this->document->valid()) { + $this->document->next(); + } return $this->document->current(); } - //Workaround for SplFileObject::seek bug in PHP7.2+ see https://bugs.php.net/bug.php?id=75917 - while ($offset !== $this->document->key() && $this->document->valid()) { - $this->document->next(); - } + $this->document->seek($offset); return $this->document->current(); } From 90bedd96fa3e3d2a988be985b9b1ee29cdb38301 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Mon, 5 Feb 2018 15:07:11 +0100 Subject: [PATCH 013/858] prepare 9.1.2 release --- CHANGELOG.md | 4 ++-- src/Reader.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8516f9e..9df176ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All Notable changes to `Csv` will be documented in this file -## Next - TBD +## 9.1.2 - 2018-02-05 ### Added @@ -15,7 +15,7 @@ All Notable changes to `Csv` will be documented in this file ### Fixed - `is_iterable` polyfill for PHP7.0 -- internal `Reader::setHeader` no longer throws exception because of a bug in PHP7.2+ [issue #279](https://github.com/thephpleague/csv/issues/213) +- `Reader::getHeader` no longer throws exception because of a bug in PHP7.2+ [issue #279](https://github.com/thephpleague/csv/issues/213) ### Removed diff --git a/src/Reader.php b/src/Reader.php index 8881664e..087ce90a 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.1 +* @version 9.1.2 * @package League.csv * * For the full copyright and license information, please view the LICENSE From 0051cfa5008bb6d1329cbe8cfa7be3d730baa3b6 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Tue, 6 Feb 2018 08:57:54 +0100 Subject: [PATCH 014/858] update documentation homepage --- docs/_data/project.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_data/project.yml b/docs/_data/project.yml index 7d113b76..d12dd6e0 100644 --- a/docs/_data/project.yml +++ b/docs/_data/project.yml @@ -15,7 +15,7 @@ releases: current: version: '9.0' requires: 'PHP >= 7.0.10' - latest: '9.0.1 - 2017-08-21' + latest: '9.1.2 - 2018-02-05' supported_until: 'TBD' documentation_link: '/9.0/' previous: From f2b8254e51bc968e4ccf42a812b6eec67d55494c Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Tue, 6 Feb 2018 09:33:43 +0100 Subject: [PATCH 015/858] update project version info --- CHANGELOG.md | 2 +- docs/_data/project.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9df176ef..a1e2607a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ All Notable changes to `Csv` will be documented in this file ### Fixed - `is_iterable` polyfill for PHP7.0 -- `Reader::getHeader` no longer throws exception because of a bug in PHP7.2+ [issue #279](https://github.com/thephpleague/csv/issues/213) +- `Reader::getHeader` no longer throws exception because of a bug in PHP7.2+ [issue #279](https://github.com/thephpleague/csv/issues/279) ### Removed diff --git a/docs/_data/project.yml b/docs/_data/project.yml index d12dd6e0..23ba6c7e 100644 --- a/docs/_data/project.yml +++ b/docs/_data/project.yml @@ -21,7 +21,7 @@ releases: previous: version: '8.0' requires: 'PHP >= 5.5.0' - latest: '8.2.2 - 2017-07-12' + latest: '8.2.3 - 2018-02-16' supported_until: '2018-02-18' documentation_link: '/8.0/' legacy: From 37cd7202a930b2fce9e5984c157a4a0c998bb857 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Tue, 6 Feb 2018 09:34:55 +0100 Subject: [PATCH 016/858] update project version info --- docs/_data/project.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_data/project.yml b/docs/_data/project.yml index 23ba6c7e..184a1166 100644 --- a/docs/_data/project.yml +++ b/docs/_data/project.yml @@ -21,7 +21,7 @@ releases: previous: version: '8.0' requires: 'PHP >= 5.5.0' - latest: '8.2.3 - 2018-02-16' + latest: '8.2.3 - 2018-02-06' supported_until: '2018-02-18' documentation_link: '/8.0/' legacy: From 57858a66787cd7d4d02c970df5ba76b66033abde Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Tue, 6 Feb 2018 09:38:33 +0100 Subject: [PATCH 017/858] promote ::getContent usage instead of __toString --- docs/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index 5b2f5676..754647bd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -19,7 +19,7 @@ $csv->setHeaderOffset(0); $header = $csv->getHeader(); //returns the CSV header record $records = $csv->getRecords(); //returns all the CSV records as an Iterator object -echo $csv; //returns the CSV document as a string +echo $csv->getContent(); //returns the CSV document as a string ~~~ ## Adding new CSV records is made simple @@ -47,7 +47,7 @@ $csv->insertOne($header); //insert all the records $csv->insertAll($records); -echo $csv; //returns the CSV document as a string +echo $csv->getContent(); //returns the CSV document as a string ~~~ ## Advanced CSV records selection From 3ec0750ce7578e7a36732f4615cd83514c9f33e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Unger?= Date: Tue, 20 Feb 2018 14:06:28 +0100 Subject: [PATCH 018/858] Static analysis with PHPStan --- .travis.yml | 3 ++- composer.json | 13 +++++++++++-- phpstan.src.neon | 2 ++ phpstan.tests.neon | 15 +++++++++++++++ src/AbstractCsv.php | 2 +- src/CharsetConverter.php | 10 +++++----- src/EncloseField.php | 4 ++-- src/RFC4180Field.php | 2 +- src/Statement.php | 2 +- src/Writer.php | 10 +++++----- tests/ReaderTest.php | 2 +- tests/ResultSetTest.php | 4 +--- tests/StreamWrapper.php | 2 +- 13 files changed, 48 insertions(+), 23 deletions(-) create mode 100644 phpstan.src.neon create mode 100644 phpstan.tests.neon diff --git a/.travis.yml b/.travis.yml index b393c0a1..2f9d5376 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,7 +29,8 @@ install: script: - composer phpunit + - composer phpstan after_script: - if [ "$COLLECT_COVERAGE" == "true" ]; then wget https://scrutinizer-ci.com/ocular.phar && php ocular.phar code-coverage:upload --format=php-clover build/clover.xml; fi - - if [ "$VALIDATE_CODING_STYLE" == "true" ]; then composer phpcs; fi \ No newline at end of file + - if [ "$VALIDATE_CODING_STYLE" == "true" ]; then composer phpcs; fi diff --git a/composer.json b/composer.json index 476d78ec..50a71fc4 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,10 @@ "require-dev": { "ext-curl" : "*", "friendsofphp/php-cs-fixer": "^2.0", - "phpunit/phpunit" : "^6.0" + "phpunit/phpunit" : "^6.0", + "phpstan/phpstan": "^0.9.2", + "phpstan/phpstan-strict-rules": "^0.9.0", + "phpstan/phpstan-phpunit": "^0.9.4" }, "autoload": { "psr-4": { @@ -40,7 +43,13 @@ "scripts": { "test": "phpunit --coverage-text; php-cs-fixer fix -v --diff --dry-run --allow-risky=yes;", "phpunit": "phpunit --coverage-text", - "phpcs": "php-cs-fixer fix -v --diff --dry-run --allow-risky=yes;" + "phpcs": "php-cs-fixer fix -v --diff --dry-run --allow-risky=yes;", + "phpstan-src": "phpstan analyse -l 7 -c phpstan.src.neon src", + "phpstan-tests": "phpstan analyse -l 7 -c phpstan.tests.neon tests", + "phpstan": [ + "@phpstan-src", + "@phpstan-tests" + ] }, "suggest": { "ext-iconv" : "Needed to ease transcoding CSV using iconv stream filters" diff --git a/phpstan.src.neon b/phpstan.src.neon new file mode 100644 index 00000000..96d0543a --- /dev/null +++ b/phpstan.src.neon @@ -0,0 +1,2 @@ +includes: + - vendor/phpstan/phpstan-strict-rules/rules.neon diff --git a/phpstan.tests.neon b/phpstan.tests.neon new file mode 100644 index 00000000..88c8c63d --- /dev/null +++ b/phpstan.tests.neon @@ -0,0 +1,15 @@ +includes: + - vendor/phpstan/phpstan-strict-rules/rules.neon + - vendor/phpstan/phpstan-phpunit/extension.neon + - vendor/phpstan/phpstan-phpunit/rules.neon + - vendor/phpstan/phpstan-phpunit/strictRules.neon +parameters: + ignoreErrors: + - '#Function xdebug_get_headers not found.#' + - '#Call to function is_iterable\(\) will always evaluate to false.#' + - '#Parameter \#1 \$stream of static method League\\Csv\\AbstractCsv::createFromStream\(\) expects resource, string given.#' + - '#Parameter \#2 \$special_chars of class League\\Csv\\EscapeFormula constructor expects array, array given.#' + - '#Parameter \#1 \$resource of class League\\Csv\\Stream constructor expects resource, string given.#' + - '#Parameter \#1 \$records of method League\\Csv\\CharsetConverter::convert\(\) expects array|Traversable, string given.#' + - '#Parameter \#2 \$delimiters of function League\\Csv\\delimiter_detect expects array, array given.#' + reportUnmatchedIgnoredErrors: false diff --git a/src/AbstractCsv.php b/src/AbstractCsv.php index 6c957152..101dcbe1 100644 --- a/src/AbstractCsv.php +++ b/src/AbstractCsv.php @@ -334,7 +334,7 @@ public function output(string $filename = null): int * * Adapted from Symfony\Component\HttpFoundation\ResponseHeaderBag::makeDisposition * - * @param string|null $filename CSV disposition name + * @param string $filename CSV disposition name * * @throws Exception if the submitted header is invalid according to RFC 6266 * diff --git a/src/CharsetConverter.php b/src/CharsetConverter.php index 240b894d..f0ae33c2 100644 --- a/src/CharsetConverter.php +++ b/src/CharsetConverter.php @@ -81,7 +81,7 @@ public static function addTo(AbstractCsv $csv, string $input_encoding, string $o public static function register() { $filtername = self::FILTERNAME.'.*'; - if (!in_array($filtername, stream_get_filters())) { + if (!in_array($filtername, stream_get_filters(), true)) { stream_filter_register($filtername, __CLASS__); } } @@ -208,17 +208,17 @@ public function __invoke(array $record): array /** * Walker method to convert the offset and the value of a CSV record field * - * @param string|null &$value - * @param string|int &$offset + * @param string|null $value + * @param string|int $offset */ protected function encodeField(&$value, &$offset) { if (null !== $value) { - $value = mb_convert_encoding((string) $value, $this->output_encoding, $this->input_encoding); + $value = mb_convert_encoding($value, $this->output_encoding, $this->input_encoding); } if (!is_int($offset)) { - $offset = mb_convert_encoding((string) $offset, $this->output_encoding, $this->input_encoding); + $offset = mb_convert_encoding($offset, $this->output_encoding, $this->input_encoding); } } diff --git a/src/EncloseField.php b/src/EncloseField.php index 90631233..f5f69217 100644 --- a/src/EncloseField.php +++ b/src/EncloseField.php @@ -75,7 +75,7 @@ public static function getFiltername(): string */ public static function register() { - if (!in_array(self::FILTERNAME, stream_get_filters())) { + if (!in_array(self::FILTERNAME, stream_get_filters(), true)) { stream_filter_register(self::FILTERNAME, __CLASS__); } } @@ -88,7 +88,7 @@ public static function register() * * @throws InvalidArgumentException if the sequence is malformed * - * @return AbstractCsv + * @return Writer */ public static function addTo(Writer $csv, string $sequence): Writer { diff --git a/src/RFC4180Field.php b/src/RFC4180Field.php index 039e5d96..62229dde 100644 --- a/src/RFC4180Field.php +++ b/src/RFC4180Field.php @@ -127,7 +127,7 @@ public static function addFormatterTo(Writer $csv, string $whitespace_replace): */ public static function register() { - if (!in_array(self::FILTERNAME, stream_get_filters())) { + if (!in_array(self::FILTERNAME, stream_get_filters(), true)) { stream_filter_register(self::FILTERNAME, __CLASS__); } } diff --git a/src/Statement.php b/src/Statement.php index 44046ba7..0b1aa40c 100644 --- a/src/Statement.php +++ b/src/Statement.php @@ -89,7 +89,7 @@ public function orderBy(callable $callable): self /** * Set LimitIterator Offset * - * @param $offset + * @param int $offset * * @throws Exception if the offset is lesser than 0 * diff --git a/src/Writer.php b/src/Writer.php index 6d9fe70f..c52e24be 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -115,7 +115,8 @@ public function insertAll($records): int /** * Adds a single record to a CSV document * - * @param string[] $record an array + * @param array $record An array consisting of nulls or values that can be converted to string (scalar string, int, + * float, objects implementing __toString() method. * * @throws CannotInsertRecord If the record can not be inserted * @@ -126,11 +127,10 @@ public function insertOne(array $record): int $record = array_reduce($this->formatters, [$this, 'formatRecord'], $record); $this->validateRecord($record); $bytes = $this->document->fputcsv($record, $this->delimiter, $this->enclosure, $this->escape); - if (!$bytes) { - throw CannotInsertRecord::triggerOnInsertion($record); + if (false !== $bytes) { + return $bytes + $this->consolidate(); } - - return $bytes + $this->consolidate(); + throw CannotInsertRecord::triggerOnInsertion($record); } /** diff --git a/tests/ReaderTest.php b/tests/ReaderTest.php index 84987926..2f7ea7b4 100644 --- a/tests/ReaderTest.php +++ b/tests/ReaderTest.php @@ -76,7 +76,7 @@ public function testGetIterator() $this->csv->setHeaderOffset(null); foreach ($this->csv->getRecords() as $record) { - $this->assertTrue(in_array(count($record), [3, 4])); + $this->assertTrue(in_array(count($record), [3, 4], true)); } } diff --git a/tests/ResultSetTest.php b/tests/ResultSetTest.php index 081aeb57..a1b88f01 100644 --- a/tests/ResultSetTest.php +++ b/tests/ResultSetTest.php @@ -160,7 +160,7 @@ public function testIntervalThrowException() public function testFilter() { $func = function ($row) { - return !in_array('jane', $row); + return !in_array('jane', $row, true); }; $this->assertNotContains( @@ -230,8 +230,6 @@ public function invalidFieldNameProvider() * @covers ::fetchColumn * @covers ::getColumnIndexByKey * @covers League\Csv\MapIterator - * - * @param int|string $field */ public function testFetchColumnTriggersOutOfRangeException() { diff --git a/tests/StreamWrapper.php b/tests/StreamWrapper.php index e2c8ff09..e374f3ba 100644 --- a/tests/StreamWrapper.php +++ b/tests/StreamWrapper.php @@ -23,7 +23,7 @@ final class StreamWrapper public static function register() { - if (!in_array(self::PROTOCOL, stream_get_wrappers())) { + if (!in_array(self::PROTOCOL, stream_get_wrappers(), true)) { stream_wrapper_register(self::PROTOCOL, __CLASS__); } } From 207be71fa5ad0f0a56f0c8a52d388cd172ed1210 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Fri, 23 Feb 2018 13:57:17 +0100 Subject: [PATCH 019/858] Improve PHPStan integration --- .travis.yml | 10 +++++----- composer.json | 11 +++++++++-- src/Stream.php | 2 +- src/Writer.php | 27 +++++++++++++++++++++------ tests/WriterTest.php | 10 ++-------- 5 files changed, 38 insertions(+), 22 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2f9d5376..408252e7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,13 +5,13 @@ sudo: false matrix: include: - php: 7.0 - env: COLLECT_COVERAGE=true VALIDATE_CODING_STYLE=true IGNORE_PLATFORMS=false + env: COLLECT_COVERAGE=false VALIDATE_CODING_STYLE=false RUN_PHPSTAN=false IGNORE_PLATFORMS=false - php: 7.1 - env: COLLECT_COVERAGE=true VALIDATE_CODING_STYLE=true IGNORE_PLATFORMS=false + env: COLLECT_COVERAGE=true VALIDATE_CODING_STYLE=false RUN_PHPSTAN=false IGNORE_PLATFORMS=false - php: 7.2 - env: COLLECT_COVERAGE=false VALIDATE_CODING_STYLE=false IGNORE_PLATFORMS=true + env: COLLECT_COVERAGE=true VALIDATE_CODING_STYLE=true RUN_PHPSTAN=true IGNORE_PLATFORMS=false - php: nightly - env: COLLECT_COVERAGE=false VALIDATE_CODING_STYLE=false IGNORE_PLATFORMS=true + env: COLLECT_COVERAGE=false VALIDATE_CODING_STYLE=false RUN_PHPSTAN=false IGNORE_PLATFORMS=true allow_failures: - php: nightly fast_finish: true @@ -29,8 +29,8 @@ install: script: - composer phpunit - - composer phpstan after_script: - if [ "$COLLECT_COVERAGE" == "true" ]; then wget https://scrutinizer-ci.com/ocular.phar && php ocular.phar code-coverage:upload --format=php-clover build/clover.xml; fi - if [ "$VALIDATE_CODING_STYLE" == "true" ]; then composer phpcs; fi + - if [ "$RUN_PHPSTAN" == "true" ]; then composer phpstan; fi diff --git a/composer.json b/composer.json index 50a71fc4..d394ff0d 100644 --- a/composer.json +++ b/composer.json @@ -41,14 +41,18 @@ } }, "scripts": { - "test": "phpunit --coverage-text; php-cs-fixer fix -v --diff --dry-run --allow-risky=yes;", - "phpunit": "phpunit --coverage-text", "phpcs": "php-cs-fixer fix -v --diff --dry-run --allow-risky=yes;", "phpstan-src": "phpstan analyse -l 7 -c phpstan.src.neon src", "phpstan-tests": "phpstan analyse -l 7 -c phpstan.tests.neon tests", "phpstan": [ "@phpstan-src", "@phpstan-tests" + ], + "phpunit": "phpunit --coverage-text", + "test": [ + "@phpunit", + "@phpcs", + "@phpstan" ] }, "suggest": { @@ -58,5 +62,8 @@ "branch-alias": { "dev-master": "9.x-dev" } + }, + "config": { + "sort-packages": true } } diff --git a/src/Stream.php b/src/Stream.php index 28e3d441..ec4968d9 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -301,7 +301,7 @@ public function setFlags(int $flags) */ public function fputcsv(array $fields, string $delimiter = ',', string $enclosure = '"', string $escape = '\\') { - $controls = $this->filterControl($delimiter, $enclosure, $escape, __METHOD__); + $controls = $this->filterControl($delimiter, $enclosure, $escape, __METHOD__); return fputcsv($this->stream, $fields, ...$controls); } diff --git a/src/Writer.php b/src/Writer.php index c52e24be..9d0fa832 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -89,7 +89,7 @@ public function getFlushThreshold() /** * Adds multiple records to the CSV document * - * a simple wrapper method around insertOne + * @see Writer::insertOne * * @param Traversable|array $records a multidimensional array or a Traversable object * @@ -115,8 +115,10 @@ public function insertAll($records): int /** * Adds a single record to a CSV document * - * @param array $record An array consisting of nulls or values that can be converted to string (scalar string, int, - * float, objects implementing __toString() method. + * @param array $record An array containing + * - scalar types values, + * - NULL values, + * - or objects implementing the __toString() method. * * @throws CannotInsertRecord If the record can not be inserted * @@ -130,16 +132,26 @@ public function insertOne(array $record): int if (false !== $bytes) { return $bytes + $this->consolidate(); } + throw CannotInsertRecord::triggerOnInsertion($record); } /** * Format a record * - * @param string[] $record + * The returned array must contain + * - scalar types values, + * - NULL values, + * - or objects implementing the __toString() method. + * + * @param array $record An array containing + * - scalar types values, + * - NULL values, + * - implementing the __toString() method. + * * @param callable $formatter * - * @return string[] + * @return array */ protected function formatRecord(array $record, callable $formatter): array { @@ -149,7 +161,10 @@ protected function formatRecord(array $record, callable $formatter): array /** * Validate a record * - * @param string[] $record + * @param array $record An array containing + * - scalar types values, + * - NULL values + * - or objects implementing __toString() method. * * @throws CannotInsertRecord If the validation failed */ diff --git a/tests/WriterTest.php b/tests/WriterTest.php index 909df9b8..373b225e 100644 --- a/tests/WriterTest.php +++ b/tests/WriterTest.php @@ -101,17 +101,11 @@ public function testInsertNormalFile() /** * @covers ::insertOne - * @covers League\Csv\CannotInsertRecord */ public function testInsertThrowsExceptionOnError() { - $expected = ['jane', 'doe', 'jane.doe@example.com']; - try { - $csv = Writer::createFromPath(__DIR__.'/data/foo.csv', 'r'); - $csv->insertOne($expected); - } catch (CannotInsertRecord $e) { - $this->assertSame($e->getRecord(), $expected); - } + $csv = Writer::createFromPath(__DIR__.'/data/foo.csv', 'r'); + $this->assertSame(0, $csv->insertOne(['jane', 'doe', 'jane.doe@example.com'])); } /** From aff9fab049c19b3d24d293cf201b1e47dd77604f Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Fri, 23 Feb 2018 14:27:46 +0100 Subject: [PATCH 020/858] adding PHPStan to relevant document --- .github/CONTRIBUTING.md | 3 +-- README.md | 8 +++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 387341a2..fabd1f1f 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -39,11 +39,10 @@ We accept contributions via Pull Requests on [Github](https://github.com/thephpl - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before submitting. -## Running Tests +- **Run the test suite** - If you want your PR to be reviewed ASAP please consider runing this command before submitting it. ``` bash $ composer test ``` - **Happy coding**! diff --git a/README.md b/README.md index d29b75da..076936e3 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,13 @@ if (!ini_get("auto_detect_line_endings")) { Testing ------- -`Csv` has a [PHPUnit](https://phpunit.de) test suite and a coding style compliance test suite using [PHP CS Fixer](http://cs.sensiolabs.org/). To run the tests, run the following command from the project folder. +`League\Csv` has a : + +- a [PHPUnit](https://phpunit.de) test suite +- a coding style compliance test suite using [PHP CS Fixer](http://cs.sensiolabs.org/). +- a code analysis compliance test suite using [PHPStan](https://github.com/phpstan/phpstan). + +To run the tests, run the following command from the project folder. ``` bash $ composer test From 26a3d862f95c81ccbdb80d0f754e0fdd7c4532cf Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Wed, 28 Feb 2018 10:49:17 +0100 Subject: [PATCH 021/858] improve gitattributes --- .gitattributes | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/.gitattributes b/.gitattributes index b8c23c0b..f8587238 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,15 +1,16 @@ * text=auto -/.editorconfig export-ignore -/.gitattributes export-ignore -/.github export-ignore -/.gitignore export-ignore -/.php_cs export-ignore -/.scrutinizer.yml export-ignore -/.travis.yml export-ignore -/docs export-ignore -/CHANGELOG.md export-ignore -/CONDUCT.md export-ignore -/README.md export-ignore -/phpunit.xml export-ignore -/tests export-ignore +/.editorconfig export-ignore +/.gitattributes export-ignore +/.github export-ignore +/.gitignore export-ignore +/.php_cs export-ignore +/.scrutinizer.yml export-ignore +/.travis.yml export-ignore +/docs export-ignore +/phpstan.src.neon export-ignore +/phpstan.tests.neon export-ignore +/phpunit.xml export-ignore +/tests export-ignore +/CONDUCT.md export-ignore +/README.md export-ignore From 2e412a3ac1147ce6ffedaae31628f18e2237913d Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Mon, 5 Mar 2018 12:51:11 +0200 Subject: [PATCH 022/858] Fix return type hint for all named constructors Fix 9.x issue on return type hint for all named constructors. --- .gitignore | 3 ++- src/AbstractCsv.php | 8 ++++---- src/Reader.php | 2 +- src/Stream.php | 4 ++-- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 039afea6..a73932c0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ build composer.lock docs/_site vendor -/nbproject/private/ \ No newline at end of file +/nbproject/private/ +.php_cs.cache diff --git a/src/AbstractCsv.php b/src/AbstractCsv.php index 101dcbe1..b2594b82 100644 --- a/src/AbstractCsv.php +++ b/src/AbstractCsv.php @@ -117,7 +117,7 @@ public function __clone() * * @return static */ - public static function createFromFileObject(SplFileObject $file): self + public static function createFromFileObject(SplFileObject $file) { return new static($file); } @@ -129,7 +129,7 @@ public static function createFromFileObject(SplFileObject $file): self * * @return static */ - public static function createFromStream($stream): self + public static function createFromStream($stream) { return new static(new Stream($stream)); } @@ -141,7 +141,7 @@ public static function createFromStream($stream): self * * @return static */ - public static function createFromString(string $content): self + public static function createFromString(string $content) { return new static(Stream::createFromString($content)); } @@ -155,7 +155,7 @@ public static function createFromString(string $content): self * * @return static */ - public static function createFromPath(string $path, string $open_mode = 'r+', $context = null): self + public static function createFromPath(string $path, string $open_mode = 'r+', $context = null) { return new static(Stream::createFromPath($path, $open_mode, $context)); } diff --git a/src/Reader.php b/src/Reader.php index 087ce90a..727520c5 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -64,7 +64,7 @@ class Reader extends AbstractCsv implements Countable, IteratorAggregate, JsonSe /** * {@inheritdoc} */ - public static function createFromPath(string $path, string $open_mode = 'r', $context = null): AbstractCsv + public static function createFromPath(string $path, string $open_mode = 'r', $context = null) { return new static(Stream::createFromPath($path, $open_mode, $context)); } diff --git a/src/Stream.php b/src/Stream.php index ec4968d9..20338a2e 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -167,7 +167,7 @@ public function __debugInfo() * * @return static */ - public static function createFromPath(string $path, string $open_mode = 'r', $context = null): self + public static function createFromPath(string $path, string $open_mode = 'r', $context = null) { $args = [$path, $open_mode]; if (null !== $context) { @@ -192,7 +192,7 @@ public static function createFromPath(string $path, string $open_mode = 'r', $co * * @return static */ - public static function createFromString(string $content): self + public static function createFromString(string $content) { $resource = fopen('php://temp', 'r+'); fwrite($resource, $content); From e77a0bc9e833a1c26289daf6663b6122dd9240d3 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Mon, 5 Mar 2018 12:16:07 +0100 Subject: [PATCH 023/858] prepare 9.1.3 release --- CHANGELOG.md | 20 ++++++++++++++++++++ src/AbstractCsv.php | 2 +- src/ByteSequence.php | 2 +- src/CannotInsertRecord.php | 2 +- src/CharsetConverter.php | 4 ++-- src/ColumnConsistency.php | 2 +- src/EncloseField.php | 2 +- src/EscapeFormula.php | 2 +- src/Exception.php | 2 +- src/HTMLConverter.php | 2 +- src/MapIterator.php | 2 +- src/RFC4180Field.php | 2 +- src/Reader.php | 2 +- src/ResultSet.php | 2 +- src/Statement.php | 2 +- src/Stream.php | 4 ++-- src/Writer.php | 6 +++--- src/XMLConverter.php | 4 ++-- src/functions.php | 2 +- tests/CsvTest.php | 23 ++++++----------------- 20 files changed, 49 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1e2607a..903b8001 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ All Notable changes to `Csv` will be documented in this file +## Next - TBD + +### Added + +- Nothing + +### Deprecated + +- Nothing + +### Fixed + +- `Writer::insertOne` allow empty array to be added to the CSV (allow inserting empty row) +- Removed all return type from named constructor see [#285](https://github.com/thephpleague/csv/pull/285) +- Added PHPStan for static code analysis + +### Removed + +- Nothing + ## 9.1.2 - 2018-02-05 ### Added diff --git a/src/AbstractCsv.php b/src/AbstractCsv.php index b2594b82..5adaf6e8 100644 --- a/src/AbstractCsv.php +++ b/src/AbstractCsv.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.2 +* @version 9.1.3 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/ByteSequence.php b/src/ByteSequence.php index 695f3acf..62012c09 100644 --- a/src/ByteSequence.php +++ b/src/ByteSequence.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.2 +* @version 9.1.3 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/CannotInsertRecord.php b/src/CannotInsertRecord.php index a85922fd..8aa14cd9 100644 --- a/src/CannotInsertRecord.php +++ b/src/CannotInsertRecord.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.2 +* @version 9.1.3 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/CharsetConverter.php b/src/CharsetConverter.php index f0ae33c2..671839fd 100644 --- a/src/CharsetConverter.php +++ b/src/CharsetConverter.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.2 +* @version 9.1.3 * @package League.csv * * For the full copyright and license information, please view the LICENSE @@ -176,7 +176,7 @@ public function filter($in, $out, &$consumed, $closing) */ public function convert($records) { - if (!is_iterable($records)) { + if (!\is_iterable($records)) { throw new TypeError(sprintf('%s() expects argument passed to be iterable, %s given', __METHOD__, gettype($records))); } diff --git a/src/ColumnConsistency.php b/src/ColumnConsistency.php index 86af082e..a03b3c0a 100644 --- a/src/ColumnConsistency.php +++ b/src/ColumnConsistency.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.2 +* @version 9.1.3 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/EncloseField.php b/src/EncloseField.php index f5f69217..461be8d1 100644 --- a/src/EncloseField.php +++ b/src/EncloseField.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.2 +* @version 9.1.3 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/EscapeFormula.php b/src/EscapeFormula.php index 714de111..bff9436d 100644 --- a/src/EscapeFormula.php +++ b/src/EscapeFormula.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.2 +* @version 9.1.3 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/Exception.php b/src/Exception.php index 1eeb75f7..2c283c18 100644 --- a/src/Exception.php +++ b/src/Exception.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.2 +* @version 9.1.3 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/HTMLConverter.php b/src/HTMLConverter.php index 687dc113..b1fa7224 100644 --- a/src/HTMLConverter.php +++ b/src/HTMLConverter.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.2 +* @version 9.1.3 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/MapIterator.php b/src/MapIterator.php index c4504fe2..83d0271d 100644 --- a/src/MapIterator.php +++ b/src/MapIterator.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.2 +* @version 9.1.3 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/RFC4180Field.php b/src/RFC4180Field.php index 62229dde..e7581b3e 100644 --- a/src/RFC4180Field.php +++ b/src/RFC4180Field.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.2 +* @version 9.1.3 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/Reader.php b/src/Reader.php index 727520c5..15726731 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.2 +* @version 9.1.3 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/ResultSet.php b/src/ResultSet.php index 679048d1..7f507d6c 100644 --- a/src/ResultSet.php +++ b/src/ResultSet.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.2 +* @version 9.1.3 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/Statement.php b/src/Statement.php index 0b1aa40c..4441ffe4 100644 --- a/src/Statement.php +++ b/src/Statement.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.2 +* @version 9.1.3 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/Stream.php b/src/Stream.php index 20338a2e..dae1ae68 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.2 +* @version 9.1.3 * @package League.csv * * For the full copyright and license information, please view the LICENSE @@ -297,7 +297,7 @@ public function setFlags(int $flags) * @param string $enclosure * @param string $escape * - * @return int|bool + * @return int|null|bool */ public function fputcsv(array $fields, string $delimiter = ',', string $enclosure = '"', string $escape = '\\') { diff --git a/src/Writer.php b/src/Writer.php index 9d0fa832..645a0876 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.2 +* @version 9.1.3 * @package League.csv * * For the full copyright and license information, please view the LICENSE @@ -97,7 +97,7 @@ public function getFlushThreshold() */ public function insertAll($records): int { - if (!is_iterable($records)) { + if (!\is_iterable($records)) { throw new TypeError(sprintf('%s() expects argument passed to be iterable, %s given', __METHOD__, gettype($records))); } @@ -129,7 +129,7 @@ public function insertOne(array $record): int $record = array_reduce($this->formatters, [$this, 'formatRecord'], $record); $this->validateRecord($record); $bytes = $this->document->fputcsv($record, $this->delimiter, $this->enclosure, $this->escape); - if (false !== $bytes) { + if (false !== $bytes && null !== $bytes) { return $bytes + $this->consolidate(); } diff --git a/src/XMLConverter.php b/src/XMLConverter.php index 76363c6b..77c63f3c 100644 --- a/src/XMLConverter.php +++ b/src/XMLConverter.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.2 +* @version 9.1.3 * @package League.csv * * For the full copyright and license information, please view the LICENSE @@ -90,7 +90,7 @@ class XMLConverter */ public function convert($records): DOMDocument { - if (!is_iterable($records)) { + if (!\is_iterable($records)) { throw new TypeError(sprintf('%s() expects argument passed to be iterable, %s given', __METHOD__, gettype($records))); } diff --git a/src/functions.php b/src/functions.php index 63a6d7b1..40f936e0 100644 --- a/src/functions.php +++ b/src/functions.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.2 +* @version 9.1.3 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/tests/CsvTest.php b/tests/CsvTest.php index a85f4070..9e72c687 100644 --- a/tests/CsvTest.php +++ b/tests/CsvTest.php @@ -7,7 +7,6 @@ use League\Csv\Writer; use PHPUnit\Framework\TestCase; use SplTempFileObject; -use TypeError; /** * @group csv @@ -66,16 +65,6 @@ public function testCreateFromPathThrowsRuntimeException() Reader::createFromPath(__DIR__.'/foo/bar', 'r'); } - /** - * @covers ::createFromStream - */ - public function testCreateFromStreamWithInvalidParameter() - { - $this->expectException(TypeError::class); - $path = __DIR__.'/data/foo.csv'; - Reader::createFromStream($path); - } - /** * @covers ::getInputBOM * @@ -384,13 +373,13 @@ public function testIsIterablePolyFill() $this->markTestSkipped('Polyfill for PHP7.0'); } - $this->assertTrue(\is_iterable(['foo'])); - $this->assertTrue(\is_iterable(Reader::createFromString(''))); - $this->assertTrue(\is_iterable((function () { + $this->assertTrue(is_iterable(['foo'])); + $this->assertTrue(is_iterable(Reader::createFromString(''))); + $this->assertTrue(is_iterable((function () { yield 1; })())); - $this->assertFalse(\is_iterable(1)); - $this->assertFalse(\is_iterable((object) ['foo'])); - $this->assertFalse(\is_iterable(Writer::createFromString(''))); + $this->assertFalse(is_iterable(1)); + $this->assertFalse(is_iterable((object) ['foo'])); + $this->assertFalse(is_iterable(Writer::createFromString(''))); } } From 0d0b12f1a0093a6c39014a5d118f6ba4274539ee Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Mon, 12 Mar 2018 08:20:01 +0100 Subject: [PATCH 024/858] release 9.1.3 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 903b8001..6229b0fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All Notable changes to `Csv` will be documented in this file -## Next - TBD +## 9.1.3 - 2018-03-12 ### Added From fa94758f5ba0fe4d8df3dd73544cd3370ff512ae Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Mon, 26 Mar 2018 13:21:54 +0200 Subject: [PATCH 025/858] bug fix issue #287 (#288) * bug fix issue #287 --- .scrutinizer.yml | 25 +++++++++++++++++----- .travis.yml | 4 ++-- src/CharsetConverter.php | 12 +++++------ src/Writer.php | 2 +- tests/CharsetConverterTest.php | 39 ++++++++++++++++++++++++++++++++++ 5 files changed, 68 insertions(+), 14 deletions(-) diff --git a/.scrutinizer.yml b/.scrutinizer.yml index 01162324..7831329e 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -1,9 +1,20 @@ +build: + nodes: + analysis: + project_setup: + override: true + tests: + override: + - php-scrutinizer-run --enable-security-analysis filter: - paths: [src/*] - excluded_paths: [tests/*] + paths: + - src/ + excluded_paths: + - tests/ checks: php: code_rating: true + duplication: true tools: external_code_coverage: timeout: 600 @@ -11,7 +22,11 @@ tools: php_code_coverage: false php_loc: enabled: true - excluded_dirs: [tests, vendor] - php_cpd: + excluded_dirs: + - tests/ + - vendor/ + php_cpd: false + php_sim: enabled: true - excluded_dirs: [tests, vendor] + filter: + paths: ['src/'] \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 408252e7..a0065fad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,8 +29,8 @@ install: script: - composer phpunit + - if [ "$VALIDATE_CODING_STYLE" == "true" ]; then composer phpcs; fi + - if [ "$RUN_PHPSTAN" == "true" ]; then composer phpstan; fi after_script: - if [ "$COLLECT_COVERAGE" == "true" ]; then wget https://scrutinizer-ci.com/ocular.phar && php ocular.phar code-coverage:upload --format=php-clover build/clover.xml; fi - - if [ "$VALIDATE_CODING_STYLE" == "true" ]; then composer phpcs; fi - - if [ "$RUN_PHPSTAN" == "true" ]; then composer phpstan; fi diff --git a/src/CharsetConverter.php b/src/CharsetConverter.php index 671839fd..99600608 100644 --- a/src/CharsetConverter.php +++ b/src/CharsetConverter.php @@ -208,17 +208,17 @@ public function __invoke(array $record): array /** * Walker method to convert the offset and the value of a CSV record field * - * @param string|null $value - * @param string|int $offset + * @param mixed $value + * @param mixed $offset */ protected function encodeField(&$value, &$offset) { - if (null !== $value) { - $value = mb_convert_encoding($value, $this->output_encoding, $this->input_encoding); + if (null !== $value && !is_numeric($value)) { + $value = mb_convert_encoding((string) $value, $this->output_encoding, $this->input_encoding); } - if (!is_int($offset)) { - $offset = mb_convert_encoding($offset, $this->output_encoding, $this->input_encoding); + if (!is_numeric($offset)) { + $offset = mb_convert_encoding((string) $offset, $this->output_encoding, $this->input_encoding); } } diff --git a/src/Writer.php b/src/Writer.php index 645a0876..557f0342 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -129,7 +129,7 @@ public function insertOne(array $record): int $record = array_reduce($this->formatters, [$this, 'formatRecord'], $record); $this->validateRecord($record); $bytes = $this->document->fputcsv($record, $this->delimiter, $this->enclosure, $this->escape); - if (false !== $bytes && null !== $bytes) { + if ('' !== (string) $bytes) { return $bytes + $this->consolidate(); } diff --git a/tests/CharsetConverterTest.php b/tests/CharsetConverterTest.php index 3bee2f7d..6f4f8e48 100644 --- a/tests/CharsetConverterTest.php +++ b/tests/CharsetConverterTest.php @@ -150,4 +150,43 @@ public function testOnCreateFailedWithWrongParams() $converter->filtername = CharsetConverter::FILTERNAME.'.foo/bar'; $this->assertFalse($converter->onCreate()); } + + /** + * @covers ::convert + * @covers ::encodeField + * + * @dataProvider converterProvider + * @param array $record + * @param array $expected + */ + public function testConvertOnlyStringField(array $record, array $expected) + { + $converter = (new CharsetConverter()) + ->inputEncoding('iso-8859-15') + ->outputEncoding('utf-8'); + $res = $converter->convert([$record]); + $this->assertSame($expected, $res[0]); + } + + public function converterProvider() + { + return [ + 'only numeric values' => [ + 'record' => [1, 2, 3], + 'expected' => [1, 2, 3], + ], + 'only string values' => [ + 'record' => ['1', '2', '3'], + 'expected' => ['1', '2', '3'], + ], + 'mixed values' => [ + 'record' => [1, '2', 3], + 'expected' => [1, '2', 3], + ], + 'mixed offset' => [ + 'record' => [1 => 1, '2' => '2', 3 => 3], + 'expected' => [1 => 1, '2' => '2', 3 => 3], + ], + ]; + } } From b9a4d39d8f8952947a7a5954489174bdd0104af5 Mon Sep 17 00:00:00 2001 From: Krzysztof Boduch Date: Mon, 26 Mar 2018 13:22:25 +0200 Subject: [PATCH 026/858] setFlushThreshold method fix (#290) setFlushThreshold method fix (#290) --- src/Writer.php | 2 +- tests/WriterTest.php | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Writer.php b/src/Writer.php index 557f0342..934b0e37 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -265,7 +265,7 @@ public function setFlushThreshold($threshold): self throw new TypeError(sprintf(__METHOD__.'() expects 1 Argument to be null or an integer %s given', gettype($threshold))); } - if (null !== $threshold && 1 >= $threshold) { + if (null !== $threshold && 1 > $threshold) { throw new Exception(__METHOD__.'() expects 1 Argument to be null or a valid integer greater or equal to 1'); } diff --git a/tests/WriterTest.php b/tests/WriterTest.php index 373b225e..5d056b06 100644 --- a/tests/WriterTest.php +++ b/tests/WriterTest.php @@ -40,10 +40,17 @@ public function tearDown() */ public function testflushThreshold() { - $this->expectException(Exception::class); $this->csv->setFlushThreshold(12); $this->assertSame(12, $this->csv->getFlushThreshold()); - $this->csv->setFlushThreshold(12); + } + + /** + * @covers ::setFlushThreshold + */ + public function testflushThresholdThrowsException() + { + $this->csv->setFlushThreshold(1); + $this->expectException(Exception::class); $this->csv->setFlushThreshold(0); } From 9c8ad06fb5d747c149875beb6133566c00eaa481 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Tue, 1 May 2018 20:32:48 +0200 Subject: [PATCH 027/858] prepare 9.1.4 release --- CHANGELOG.md | 20 ++++++++++++++++++++ docs/_data/project.yml | 2 +- src/AbstractCsv.php | 2 +- src/ByteSequence.php | 2 +- src/CannotInsertRecord.php | 2 +- src/CharsetConverter.php | 2 +- src/ColumnConsistency.php | 2 +- src/EncloseField.php | 2 +- src/EscapeFormula.php | 2 +- src/Exception.php | 2 +- src/HTMLConverter.php | 2 +- src/MapIterator.php | 2 +- src/RFC4180Field.php | 2 +- src/Reader.php | 2 +- src/ResultSet.php | 2 +- src/Statement.php | 2 +- src/Stream.php | 2 +- src/Writer.php | 2 +- src/XMLConverter.php | 2 +- src/functions.php | 2 +- 20 files changed, 39 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6229b0fb..970d70ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ All Notable changes to `Csv` will be documented in this file +## 9.1.4 - 2018-05-01 + +### Added + +- Nothing + +### Deprecated + +- Nothing + +### Fixed + +- `Writer::setFlushThreshold` should accept 1 as an argument [#289(https://github.com/thephpleague/csv/issue/289) + +- `CharsetConverter::convert` should not try to convert numeric value [#287](https://github.com/thephpleague/csv/issue/287) + +### Removed + +- Nothing + ## 9.1.3 - 2018-03-12 ### Added diff --git a/docs/_data/project.yml b/docs/_data/project.yml index 184a1166..e01318c4 100644 --- a/docs/_data/project.yml +++ b/docs/_data/project.yml @@ -15,7 +15,7 @@ releases: current: version: '9.0' requires: 'PHP >= 7.0.10' - latest: '9.1.2 - 2018-02-05' + latest: '9.1.4 - 2018-05-01' supported_until: 'TBD' documentation_link: '/9.0/' previous: diff --git a/src/AbstractCsv.php b/src/AbstractCsv.php index 5adaf6e8..655e74a6 100644 --- a/src/AbstractCsv.php +++ b/src/AbstractCsv.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.3 +* @version 9.1.4 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/ByteSequence.php b/src/ByteSequence.php index 62012c09..8089dae2 100644 --- a/src/ByteSequence.php +++ b/src/ByteSequence.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.3 +* @version 9.1.4 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/CannotInsertRecord.php b/src/CannotInsertRecord.php index 8aa14cd9..18b99696 100644 --- a/src/CannotInsertRecord.php +++ b/src/CannotInsertRecord.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.3 +* @version 9.1.4 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/CharsetConverter.php b/src/CharsetConverter.php index 99600608..003c7f43 100644 --- a/src/CharsetConverter.php +++ b/src/CharsetConverter.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.3 +* @version 9.1.4 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/ColumnConsistency.php b/src/ColumnConsistency.php index a03b3c0a..d6fc53bc 100644 --- a/src/ColumnConsistency.php +++ b/src/ColumnConsistency.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.3 +* @version 9.1.4 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/EncloseField.php b/src/EncloseField.php index 461be8d1..ef031e82 100644 --- a/src/EncloseField.php +++ b/src/EncloseField.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.3 +* @version 9.1.4 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/EscapeFormula.php b/src/EscapeFormula.php index bff9436d..f83c7aec 100644 --- a/src/EscapeFormula.php +++ b/src/EscapeFormula.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.3 +* @version 9.1.4 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/Exception.php b/src/Exception.php index 2c283c18..4c388275 100644 --- a/src/Exception.php +++ b/src/Exception.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.3 +* @version 9.1.4 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/HTMLConverter.php b/src/HTMLConverter.php index b1fa7224..32e35e8b 100644 --- a/src/HTMLConverter.php +++ b/src/HTMLConverter.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.3 +* @version 9.1.4 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/MapIterator.php b/src/MapIterator.php index 83d0271d..7ff601e3 100644 --- a/src/MapIterator.php +++ b/src/MapIterator.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.3 +* @version 9.1.4 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/RFC4180Field.php b/src/RFC4180Field.php index e7581b3e..7b9e8abc 100644 --- a/src/RFC4180Field.php +++ b/src/RFC4180Field.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.3 +* @version 9.1.4 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/Reader.php b/src/Reader.php index 15726731..58720b1b 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.3 +* @version 9.1.4 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/ResultSet.php b/src/ResultSet.php index 7f507d6c..fa2cbb99 100644 --- a/src/ResultSet.php +++ b/src/ResultSet.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.3 +* @version 9.1.4 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/Statement.php b/src/Statement.php index 4441ffe4..b436e445 100644 --- a/src/Statement.php +++ b/src/Statement.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.3 +* @version 9.1.4 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/Stream.php b/src/Stream.php index dae1ae68..6396850d 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.3 +* @version 9.1.4 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/Writer.php b/src/Writer.php index 934b0e37..aa3edfb9 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.3 +* @version 9.1.4 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/XMLConverter.php b/src/XMLConverter.php index 77c63f3c..28f3bbfa 100644 --- a/src/XMLConverter.php +++ b/src/XMLConverter.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.3 +* @version 9.1.4 * @package League.csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/functions.php b/src/functions.php index 40f936e0..d3dd59e7 100644 --- a/src/functions.php +++ b/src/functions.php @@ -4,7 +4,7 @@ * * @license http://opensource.org/licenses/MIT * @link https://github.com/thephpleague/csv/ -* @version 9.1.3 +* @version 9.1.4 * @package League.csv * * For the full copyright and license information, please view the LICENSE From 64200e711f0ca992f331b00ec2cceccc127c21fc Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Tue, 1 May 2018 20:58:42 +0200 Subject: [PATCH 028/858] bump to next dev version --- .scrutinizer.yml | 2 +- CHANGELOG.md | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.scrutinizer.yml b/.scrutinizer.yml index 7831329e..dc476e93 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -17,7 +17,7 @@ checks: duplication: true tools: external_code_coverage: - timeout: 600 + timeout: 3600 runs: 2 php_code_coverage: false php_loc: diff --git a/CHANGELOG.md b/CHANGELOG.md index 970d70ed..1cc6ee57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ All Notable changes to `Csv` will be documented in this file +## Next - TBD + +### Added + +- Nothing + +### Deprecated + +- Nothing + +### Fixed + +- Nothing + +### Removed + +- Nothing + ## 9.1.4 - 2018-05-01 ### Added @@ -14,7 +32,7 @@ All Notable changes to `Csv` will be documented in this file ### Fixed -- `Writer::setFlushThreshold` should accept 1 as an argument [#289(https://github.com/thephpleague/csv/issue/289) +- `Writer::setFlushThreshold` should accept 1 as an argument [#289](https://github.com/thephpleague/csv/issue/289) - `CharsetConverter::convert` should not try to convert numeric value [#287](https://github.com/thephpleague/csv/issue/287) From fef5735615e5bc834a38adfcc5118d082dc85bdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Schl=C3=A4pfer?= Date: Tue, 15 May 2018 02:50:39 +0200 Subject: [PATCH 029/858] fix a typo in index.md (#294) developper --> developer --- docs/9.0/writer/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/9.0/writer/index.md b/docs/9.0/writer/index.md index 91a1d1e1..60222128 100644 --- a/docs/9.0/writer/index.md +++ b/docs/9.0/writer/index.md @@ -82,7 +82,7 @@ try { ## Handling newline -Because PHP's `fputcsv` implementation has a hardcoded `\n`, we need to be able to replace the last `LF` code with one supplied by the developper for more interoperability between CSV packages on different platforms. The newline sequence will be appended to each newly inserted CSV record. +Because PHP's `fputcsv` implementation has a hardcoded `\n`, we need to be able to replace the last `LF` code with one supplied by the developer for more interoperability between CSV packages on different platforms. The newline sequence will be appended to each newly inserted CSV record. ### Description From ecd0f897ac63cb02d5e6d3b8f992c9c5d99d4dcd Mon Sep 17 00:00:00 2001 From: jmdelehaye Date: Sun, 22 Jul 2018 20:57:21 +0200 Subject: [PATCH 030/858] Fix typo (#302) Fix typo on enclose-field link --- docs/9.0/interoperability/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/9.0/interoperability/index.md b/docs/9.0/interoperability/index.md index ef15e5db..8b5b4bc7 100644 --- a/docs/9.0/interoperability/index.md +++ b/docs/9.0/interoperability/index.md @@ -9,9 +9,9 @@ Depending on your operating system and on the software you are using to read/imp - [encoding and BOM presence](/9.0/interoperability/encoding/) - [rfc4180 field compliance](/9.0/interoperability/rfc4180-field/) -- [add the enclosure character on all fields](/9.0/interoperability/enclosure-field/) +- [add the enclosure character on all fields](/9.0/interoperability/enclose-field/) - [prevents CSV formula injection](/9.0/interoperability/escape-formula-injection/)

Out of the box, League\Csv connections do not alter the CSV document presentation and uses PHP's CSV native functions to read and write CSV records.

-In the examples we will be using an existing CSV in ISO-8859-15 charset encoding as a starting point. The code may vary if your CSV document is in a different charset and/or exposes different CSV field formats. \ No newline at end of file +In the examples we will be using an existing CSV in ISO-8859-15 charset encoding as a starting point. The code may vary if your CSV document is in a different charset and/or exposes different CSV field formats. From b0797366cce18583313c3105b670841e6f933768 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Mon, 30 Jul 2018 16:39:57 +0200 Subject: [PATCH 031/858] Update coding standard --- .php_cs | 20 +++++ .scrutinizer.yml | 29 ++----- .travis.yml | 6 +- composer.json | 2 +- src/AbstractCsv.php | 146 +++++++++++++------------------- src/ByteSequence.php | 33 ++++---- src/CannotInsertRecord.php | 47 +++++----- src/CharsetConverter.php | 99 ++++++++++------------ src/ColumnConsistency.php | 43 +++++----- src/EncloseField.php | 61 +++++++------ src/EscapeFormula.php | 61 ++++++------- src/Exception.php | 24 +++--- src/HTMLConverter.php | 54 +++++------- src/MapIterator.php | 31 ++++--- src/RFC4180Field.php | 76 ++++++++--------- src/Reader.php | 89 ++++++++++--------- src/ResultSet.php | 64 +++++++------- src/Statement.php | 82 +++++++----------- src/Stream.php | 143 +++++++++++++++---------------- src/Writer.php | 97 +++++++++------------ src/XMLConverter.php | 99 +++++++--------------- src/functions.php | 54 ++++++------ src/functions_include.php | 12 ++- tests/ByteSequenceTest.php | 13 +++ tests/CharsetConverterTest.php | 20 ++++- tests/ColumnConsistencyTest.php | 12 +++ tests/CsvTest.php | 23 +++++ tests/DetectDelimiterTest.php | 12 +++ tests/EncloseFieldTest.php | 14 ++- tests/EscapeFormulaTest.php | 12 +++ tests/HTMLConverterTest.php | 12 +++ tests/RFC4180FieldTest.php | 18 +++- tests/ReaderTest.php | 26 ++++-- tests/ResultSetTest.php | 23 ++++- tests/StreamTest.php | 19 +++++ tests/StreamWrapper.php | 28 ++++-- tests/WriterTest.php | 17 +++- tests/XMLConverterTest.php | 12 +++ 38 files changed, 850 insertions(+), 783 deletions(-) diff --git a/.php_cs b/.php_cs index bb1a8a13..2c4b840f 100644 --- a/.php_cs +++ b/.php_cs @@ -1,5 +1,17 @@ +@license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) +@version 9.1.5 +@link https://github.com/thephpleague/csv + +For the full copyright and license information, please view the LICENSE +file that was distributed with this source code. +EOF; + $finder = PhpCsFixer\Finder::create() ->in(__DIR__.'/src') ->in(__DIR__.'/tests') @@ -10,11 +22,18 @@ return PhpCsFixer\Config::create() '@PSR2' => true, 'array_syntax' => ['syntax' => 'short'], 'concat_space' => ['spacing' => 'none'], + 'header_comment' => [ + 'commentType' => 'PHPDoc', + 'header' => $header, + 'location' => 'after_open', + 'separate' => 'both', + ], 'new_with_braces' => true, 'no_blank_lines_after_phpdoc' => true, 'no_empty_phpdoc' => true, 'no_empty_comment' => true, 'no_leading_import_slash' => true, + 'no_superfluous_phpdoc_tags' => true, 'no_trailing_comma_in_singleline_array' => true, 'no_unused_imports' => true, 'ordered_imports' => ['importsOrder' => null, 'sortAlgorithm' => 'alpha'], @@ -24,6 +43,7 @@ return PhpCsFixer\Config::create() 'phpdoc_order' => true, 'phpdoc_scalar' => true, 'phpdoc_to_comment' => true, + 'phpdoc_summary' => true, 'psr0' => true, 'psr4' => true, 'return_type_declaration' => ['space_before' => 'none'], diff --git a/.scrutinizer.yml b/.scrutinizer.yml index dc476e93..c450246b 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -1,32 +1,17 @@ -build: - nodes: - analysis: - project_setup: - override: true - tests: - override: - - php-scrutinizer-run --enable-security-analysis filter: - paths: - - src/ - excluded_paths: - - tests/ + paths: [src/*] + excluded_paths: [tests/*] checks: php: code_rating: true - duplication: true tools: external_code_coverage: - timeout: 3600 - runs: 2 + timeout: 600 + runs: 3 php_code_coverage: false php_loc: enabled: true - excluded_dirs: - - tests/ - - vendor/ - php_cpd: false - php_sim: + excluded_dirs: [tests, vendor] + php_cpd: enabled: true - filter: - paths: ['src/'] \ No newline at end of file + excluded_dirs: [tests, vendor] \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index a0065fad..43ec4a78 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,11 +5,11 @@ sudo: false matrix: include: - php: 7.0 - env: COLLECT_COVERAGE=false VALIDATE_CODING_STYLE=false RUN_PHPSTAN=false IGNORE_PLATFORMS=false - - php: 7.1 env: COLLECT_COVERAGE=true VALIDATE_CODING_STYLE=false RUN_PHPSTAN=false IGNORE_PLATFORMS=false - - php: 7.2 + - php: 7.1 env: COLLECT_COVERAGE=true VALIDATE_CODING_STYLE=true RUN_PHPSTAN=true IGNORE_PLATFORMS=false + - php: 7.2 + env: COLLECT_COVERAGE=false VALIDATE_CODING_STYLE=false RUN_PHPSTAN=false IGNORE_PLATFORMS=false - php: nightly env: COLLECT_COVERAGE=false VALIDATE_CODING_STYLE=false RUN_PHPSTAN=false IGNORE_PLATFORMS=true allow_failures: diff --git a/composer.json b/composer.json index d394ff0d..fe9bce12 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ }, "require-dev": { "ext-curl" : "*", - "friendsofphp/php-cs-fixer": "^2.0", + "friendsofphp/php-cs-fixer": "^2.12", "phpunit/phpunit" : "^6.0", "phpstan/phpstan": "^0.9.2", "phpstan/phpstan-strict-rules": "^0.9.0", diff --git a/src/AbstractCsv.php b/src/AbstractCsv.php index 655e74a6..60180475 100644 --- a/src/AbstractCsv.php +++ b/src/AbstractCsv.php @@ -1,21 +1,36 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + declare(strict_types=1); namespace League\Csv; use Generator; use SplFileObject; +use const FILTER_FLAG_STRIP_HIGH; +use const FILTER_FLAG_STRIP_LOW; +use const FILTER_SANITIZE_STRING; +use function filter_var; +use function get_class; +use function implode; +use function mb_strlen; +use function rawurlencode; +use function sprintf; +use function str_replace; +use function str_split; +use function strcspn; +use function strlen; /** * An abstract class to enable CSV document loading. @@ -27,7 +42,7 @@ abstract class AbstractCsv implements ByteSequence { /** - * The stream filter mode (read or write) + * The stream filter mode (read or write). * * @var int */ @@ -35,56 +50,56 @@ abstract class AbstractCsv implements ByteSequence /** - * collection of stream filters + * collection of stream filters. * * @var bool[] */ protected $stream_filters = []; /** - * The CSV document BOM sequence + * The CSV document BOM sequence. * * @var string|null */ protected $input_bom = null; /** - * The Output file BOM character + * The Output file BOM character. * * @var string */ protected $output_bom = ''; /** - * the field delimiter (one character only) + * the field delimiter (one character only). * * @var string */ protected $delimiter = ','; /** - * the field enclosure character (one character only) + * the field enclosure character (one character only). * * @var string */ protected $enclosure = '"'; /** - * the field escape character (one character only) + * the field escape character (one character only). * * @var string */ protected $escape = '\\'; /** - * The CSV document + * The CSV document. * * @var SplFileObject|Stream */ protected $document; /** - * New instance + * New instance. * * @param SplFileObject|Stream $document The CSV Object instance */ @@ -111,9 +126,7 @@ public function __clone() } /** - * Return a new instance from a SplFileObject - * - * @param SplFileObject $file + * Return a new instance from a SplFileObject. * * @return static */ @@ -123,7 +136,7 @@ public static function createFromFileObject(SplFileObject $file) } /** - * Return a new instance from a PHP resource stream + * Return a new instance from a PHP resource stream. * * @param resource $stream * @@ -135,9 +148,7 @@ public static function createFromStream($stream) } /** - * Return a new instance from a string - * - * @param string $content the CSV document as a string + * Return a new instance from a string. * * @return static */ @@ -147,11 +158,9 @@ public static function createFromString(string $content) } /** - * Return a new instance from a file path + * Return a new instance from a file path. * - * @param string $path file path - * @param string $open_mode the file open mode flag - * @param resource|null $context the resource context + * @param resource|null $context the resource context * * @return static */ @@ -161,9 +170,7 @@ public static function createFromPath(string $path, string $open_mode = 'r+', $c } /** - * Returns the current field delimiter - * - * @return string + * Returns the current field delimiter. */ public function getDelimiter(): string { @@ -171,9 +178,7 @@ public function getDelimiter(): string } /** - * Returns the current field enclosure - * - * @return string + * Returns the current field enclosure. */ public function getEnclosure(): string { @@ -181,9 +186,7 @@ public function getEnclosure(): string } /** - * Returns the current field escape character - * - * @return string + * Returns the current field escape character. */ public function getEscape(): string { @@ -191,9 +194,7 @@ public function getEscape(): string } /** - * Returns the BOM sequence in use on Output methods - * - * @return string + * Returns the BOM sequence in use on Output methods. */ public function getOutputBOM(): string { @@ -201,9 +202,7 @@ public function getOutputBOM(): string } /** - * Returns the BOM sequence of the given CSV - * - * @return string + * Returns the BOM sequence of the given CSV. */ public function getInputBOM(): string { @@ -220,9 +219,7 @@ public function getInputBOM(): string } /** - * Returns the stream filter mode - * - * @return int + * Returns the stream filter mode. */ public function getStreamFilterMode(): int { @@ -230,9 +227,7 @@ public function getStreamFilterMode(): int } /** - * Tells whether the stream filter capabilities can be used - * - * @return bool + * Tells whether the stream filter capabilities can be used. */ public function supportsStreamFilter(): bool { @@ -240,11 +235,7 @@ public function supportsStreamFilter(): bool } /** - * Tell whether the specify stream filter is attach to the current stream - * - * @param string $filtername - * - * @return bool + * Tell whether the specify stream filter is attach to the current stream. */ public function hasStreamFilter(string $filtername): bool { @@ -252,13 +243,11 @@ public function hasStreamFilter(string $filtername): bool } /** - * Retuns the CSV document as a Generator of string chunk + * Retuns the CSV document as a Generator of string chunk. * * @param int $length number of bytes read * * @throws Exception if the number of bytes is lesser than 1 - * - * @return Generator */ public function chunk(int $length): Generator { @@ -279,14 +268,12 @@ public function chunk(int $length): Generator } /** - * DEPRECATION WARNING! This method will be removed in the next major point release + * DEPRECATION WARNING! This method will be removed in the next major point release. * * @deprecated deprecated since version 9.1.0 * @see AbstractCsv::getContent * * Retrieves the CSV content - * - * @return string */ public function __toString(): string { @@ -294,9 +281,7 @@ public function __toString(): string } /** - * Retrieves the CSV content - * - * @return string + * Retrieves the CSV content. */ public function getContent(): string { @@ -309,9 +294,9 @@ public function getContent(): string } /** - * Outputs all data on the CSV file + * Outputs all data on the CSV file. * - * @param string $filename CSV downloaded name if present adds extra headers + * @param null|string $filename * * @return int Returns the number of characters read from the handle * and passed through to the output. @@ -330,12 +315,10 @@ public function output(string $filename = null): int } /** - * Send the CSV headers + * Send the CSV headers. * * Adapted from Symfony\Component\HttpFoundation\ResponseHeaderBag::makeDisposition * - * @param string $filename CSV disposition name - * * @throws Exception if the submitted header is invalid according to RFC 6266 * * @see https://tools.ietf.org/html/rfc6266#section-4.3 @@ -365,9 +348,7 @@ protected function sendHeaders(string $filename) } /** - * Sets the field delimiter - * - * @param string $delimiter + * Sets the field delimiter. * * @throws Exception If the Csv control character is not one character only. * @@ -390,16 +371,14 @@ public function setDelimiter(string $delimiter): self } /** - * Reset dynamic object properties to improve performance + * Reset dynamic object properties to improve performance. */ protected function resetProperties() { } /** - * Sets the field enclosure - * - * @param string $enclosure + * Sets the field enclosure. * * @throws Exception If the Csv control character is not one character only. * @@ -422,9 +401,7 @@ public function setEnclosure(string $enclosure): self } /** - * Sets the field escape character - * - * @param string $escape + * Sets the field escape character. * * @throws Exception If the Csv control character is not one character only. * @@ -447,9 +424,7 @@ public function setEscape(string $escape): self } /** - * Sets the BOM sequence to prepend the CSV on output - * - * @param string $str The BOM sequence + * Sets the BOM sequence to prepend the CSV on output. * * @return static */ @@ -461,10 +436,9 @@ public function setOutputBOM(string $str): self } /** - * append a stream filter + * append a stream filter. * - * @param string $filtername a string or an object that implements the '__toString' method - * @param mixed $params additional parameters for the filter + * @param null|mixed $params * * @throws Exception If the stream filter API can not be used * diff --git a/src/ByteSequence.php b/src/ByteSequence.php index 8089dae2..13c837f8 100644 --- a/src/ByteSequence.php +++ b/src/ByteSequence.php @@ -1,20 +1,21 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ namespace League\Csv; /** - * Defines constants for common BOM sequences + * Defines constants for common BOM sequences. * * @package League.csv * @since 9.0.0 @@ -23,27 +24,27 @@ interface ByteSequence { /** - * UTF-8 BOM sequence + * UTF-8 BOM sequence. */ const BOM_UTF8 = "\xEF\xBB\xBF"; /** - * UTF-16 BE BOM sequence + * UTF-16 BE BOM sequence. */ const BOM_UTF16_BE = "\xFE\xFF"; /** - * UTF-16 LE BOM sequence + * UTF-16 LE BOM sequence. */ const BOM_UTF16_LE = "\xFF\xFE"; /** - * UTF-32 BE BOM sequence + * UTF-32 BE BOM sequence. */ const BOM_UTF32_BE = "\x00\x00\xFE\xFF"; /** - * UTF-32 LE BOM sequence + * UTF-32 LE BOM sequence. */ const BOM_UTF32_LE = "\xFF\xFE\x00\x00"; } diff --git a/src/CannotInsertRecord.php b/src/CannotInsertRecord.php index 18b99696..8abff099 100644 --- a/src/CannotInsertRecord.php +++ b/src/CannotInsertRecord.php @@ -1,21 +1,23 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + declare(strict_types=1); namespace League\Csv; /** - * Thrown when a data is not added to the Csv Document + * Thrown when a data is not added to the Csv Document. * * @package League.csv * @since 9.0.0 @@ -24,25 +26,21 @@ class CannotInsertRecord extends Exception { /** - * The record submitted for insertion + * The record submitted for insertion. * * @var array */ protected $record; /** - * Validator which did not validated the data + * Validator which did not validated the data. * * @var string */ protected $name = ''; /** - * Create an Exception from a record insertion into a stream - * - * @param string[] $record - * - * @return self + * Create an Exception from a record insertion into a stream. */ public static function triggerOnInsertion(array $record): self { @@ -53,12 +51,7 @@ public static function triggerOnInsertion(array $record): self } /** - * Create an Exception from a Record Validation - * - * @param string $name validator name - * @param string[] $record invalid data - * - * @return self + * Create an Exception from a Record Validation. */ public static function triggerOnValidation(string $name, array $record): self { @@ -70,9 +63,8 @@ public static function triggerOnValidation(string $name, array $record): self } /** - * return the validator name + * return the validator name. * - * @return string */ public function getName(): string { @@ -80,9 +72,8 @@ public function getName(): string } /** - * return the invalid data submitted + * return the invalid data submitted. * - * @return array */ public function getRecord(): array { diff --git a/src/CharsetConverter.php b/src/CharsetConverter.php index 003c7f43..5530fc81 100644 --- a/src/CharsetConverter.php +++ b/src/CharsetConverter.php @@ -1,15 +1,17 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + declare(strict_types=1); namespace League\Csv; @@ -18,9 +20,27 @@ use php_user_filter; use Traversable; use TypeError; +use function array_combine; +use function array_map; +use function array_walk; +use function gettype; +use function in_array; +use function is_iterable; +use function is_numeric; +use function mb_convert_encoding; +use function mb_list_encodings; +use function preg_match; +use function sprintf; +use function stream_bucket_append; +use function stream_bucket_make_writeable; +use function stream_filter_register; +use function stream_get_filters; +use function strpos; +use function strtolower; +use function substr; /** - * A class to convert resource stream or tabular data content charset + * A class to convert resource stream or tabular data content charset. * * @package League.csv * @since 9.0.0 @@ -31,7 +51,7 @@ class CharsetConverter extends php_user_filter const FILTERNAME = 'convert.league.csv'; /** - * the filter name used to instantiate the class with + * the filter name used to instantiate the class with. * * @var string */ @@ -39,34 +59,28 @@ class CharsetConverter extends php_user_filter /** * Contents of the params parameter passed to stream_filter_append - * or stream_filter_prepend functions + * or stream_filter_prepend functions. * * @var mixed */ public $params; /** - * The records input encoding charset + * The records input encoding charset. * * @var string */ protected $input_encoding = 'UTF-8'; /** - * The records output encoding charset + * The records output encoding charset. * * @var string */ protected $output_encoding = 'UTF-8'; /** - * Static method to add the stream filter to a {@link AbstractCsv} object - * - * @param AbstractCsv $csv - * @param string $input_encoding - * @param string $output_encoding - * - * @return AbstractCsv + * Static method to add the stream filter to a {@link AbstractCsv} object. */ public static function addTo(AbstractCsv $csv, string $input_encoding, string $output_encoding): AbstractCsv { @@ -76,7 +90,7 @@ public static function addTo(AbstractCsv $csv, string $input_encoding, string $o } /** - * Static method to register the class as a stream filter + * Static method to register the class as a stream filter. */ public static function register() { @@ -87,12 +101,7 @@ public static function register() } /** - * Static method to return the stream filter filtername - * - * @param string $input_encoding - * @param string $output_encoding - * - * @return string + * Static method to return the stream filter filtername. */ public static function getFiltername(string $input_encoding, string $output_encoding): string { @@ -105,13 +114,9 @@ public static function getFiltername(string $input_encoding, string $output_enco } /** - * Filter encoding charset - * - * @param string $encoding + * Filter encoding charset. * * @throws OutOfRangeException if the charset is malformed or unsupported - * - * @return string */ protected static function filterEncoding(string $encoding): string { @@ -168,15 +173,15 @@ public function filter($in, $out, &$consumed, $closing) } /** - * Convert Csv file into UTF-8 + * Convert Csv records collection into UTF-8. * - * @param array|Traversable $records the CSV records collection + * @param array|Traversable $records * * @return array|Traversable */ public function convert($records) { - if (!\is_iterable($records)) { + if (!is_iterable($records)) { throw new TypeError(sprintf('%s() expects argument passed to be iterable, %s given', __METHOD__, gettype($records))); } @@ -192,11 +197,7 @@ public function convert($records) } /** - * Enable using the class as a formatter for the {@link Writer} - * - * @param array $record CSV record - * - * @return array + * Enable using the class as a formatter for the {@link Writer}. */ public function __invoke(array $record): array { @@ -206,7 +207,7 @@ public function __invoke(array $record): array } /** - * Walker method to convert the offset and the value of a CSV record field + * Walker method to convert the offset and the value of a CSV record field. * * @param mixed $value * @param mixed $offset @@ -223,11 +224,7 @@ protected function encodeField(&$value, &$offset) } /** - * Sets the records input encoding charset - * - * @param string $encoding - * - * @return self + * Sets the records input encoding charset. */ public function inputEncoding(string $encoding): self { @@ -243,11 +240,7 @@ public function inputEncoding(string $encoding): self } /** - * Sets the records output encoding charset - * - * @param string $encoding - * - * @return self + * Sets the records output encoding charset. */ public function outputEncoding(string $encoding): self { diff --git a/src/ColumnConsistency.php b/src/ColumnConsistency.php index d6fc53bc..c010602e 100644 --- a/src/ColumnConsistency.php +++ b/src/ColumnConsistency.php @@ -1,21 +1,26 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + declare(strict_types=1); namespace League\Csv; +use function count; +use function sprintf; + /** - * A class to validate column consistency when inserting records into a CSV document + * A class to validate column consistency when inserting records into a CSV document. * * @package League.csv * @since 7.0.0 @@ -24,17 +29,15 @@ class ColumnConsistency { /** - * The number of column per record + * The number of column per record. * * @var int */ protected $columns_count; /** - * New Instance - * + * New Instance. * - * @param int $columns_count * @throws OutOfRangeException if the column count is lesser than -1 */ public function __construct(int $columns_count = -1) @@ -47,9 +50,7 @@ public function __construct(int $columns_count = -1) } /** - * Returns the column count - * - * @return int + * Returns the column count. */ public function getColumnCount(): int { @@ -57,11 +58,7 @@ public function getColumnCount(): int } /** - * Tell whether the submitted record is valid - * - * @param array $record - * - * @return bool + * Tell whether the submitted record is valid. */ public function __invoke(array $record): bool { diff --git a/src/EncloseField.php b/src/EncloseField.php index ef031e82..68b165b3 100644 --- a/src/EncloseField.php +++ b/src/EncloseField.php @@ -1,24 +1,34 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + declare(strict_types=1); namespace League\Csv; use InvalidArgumentException; use php_user_filter; +use function in_array; +use function str_replace; +use function strcspn; +use function stream_bucket_append; +use function stream_bucket_make_writeable; +use function stream_filter_register; +use function stream_get_filters; +use function strlen; /** - * A stream filter to improve enclosure character usage + * A stream filter to improve enclosure character usage. * * @see https://tools.ietf.org/html/rfc4180#section-2 * @see https://bugs.php.net/bug.php?id=38301 @@ -32,7 +42,7 @@ class EncloseField extends php_user_filter const FILTERNAME = 'convert.league.csv.enclosure'; /** - * the filter name used to instantiate the class with + * the filter name used to instantiate the class with. * * @var string */ @@ -40,30 +50,28 @@ class EncloseField extends php_user_filter /** * Contents of the params parameter passed to stream_filter_append - * or stream_filter_prepend functions + * or stream_filter_prepend functions. * * @var mixed */ public $params; /** - * Default sequence + * Default sequence. * * @var string */ protected $sequence; /** - * Characters that triggers enclosure in PHP + * Characters that triggers enclosure in PHP. * * @var string */ protected static $force_enclosure = "\n\r\t "; /** - * Static method to return the stream filter filtername - * - * @return string + * Static method to return the stream filter filtername. */ public static function getFiltername(): string { @@ -71,7 +79,7 @@ public static function getFiltername(): string } /** - * Static method to register the class as a stream filter + * Static method to register the class as a stream filter. */ public static function register() { @@ -81,14 +89,9 @@ public static function register() } /** - * Static method to add the stream filter to a {@link Writer} object - * - * @param Writer $csv - * @param string $sequence + * Static method to add the stream filter to a {@link Writer} object. * * @throws InvalidArgumentException if the sequence is malformed - * - * @return Writer */ public static function addTo(Writer $csv, string $sequence): Writer { @@ -113,13 +116,9 @@ public static function addTo(Writer $csv, string $sequence): Writer } /** - * Filter type and sequence parameters - * - * - The sequence to force enclosure MUST contains one of the following character ("\n\r\t ") - * - * @param string $sequence + * Filter type and sequence parameters. * - * @return bool + * The sequence to force enclosure MUST contains one of the following character ("\n\r\t ") */ protected static function isValidSequence(string $sequence): bool { diff --git a/src/EscapeFormula.php b/src/EscapeFormula.php index f83c7aec..cbf8b44e 100644 --- a/src/EscapeFormula.php +++ b/src/EscapeFormula.php @@ -1,23 +1,34 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + declare(strict_types=1); namespace League\Csv; use InvalidArgumentException; +use function array_fill_keys; +use function array_keys; +use function array_map; +use function array_merge; +use function array_unique; +use function is_object; +use function is_string; +use function method_exists; +use function sprintf; /** - * A League CSV formatter to tackle CSV Formula Injection + * A League CSV formatter to tackle CSV Formula Injection. * * @see http://georgemauer.net/2017/10/07/csv-injection.html * @@ -28,26 +39,26 @@ class EscapeFormula { /** - * Spreadsheet formula starting character + * Spreadsheet formula starting character. */ const FORMULA_STARTING_CHARS = ['=', '-', '+', '@']; /** - * Effective Spreadsheet formula starting characters + * Effective Spreadsheet formula starting characters. * * @var array */ protected $special_chars = []; /** - * Escape character to escape each CSV formula field + * Escape character to escape each CSV formula field. * * @var string */ protected $escape; /** - * New instance + * New instance. * * @param string $escape escape character to escape each CSV formula field * @param string[] $special_chars additional spreadsheet formula starting characters @@ -56,7 +67,7 @@ class EscapeFormula public function __construct(string $escape = "\t", array $special_chars = []) { $this->escape = $escape; - if (!empty($special_chars)) { + if ([] !== $special_chars) { $special_chars = $this->filterSpecialCharacters(...$special_chars); } @@ -97,8 +108,6 @@ public function getSpecialCharacters(): array /** * Returns the escape character. - * - * @return string */ public function getEscape(): string { @@ -109,10 +118,6 @@ public function getEscape(): string * League CSV formatter hook. * * @see escapeRecord - * - * @param array $record - * - * @return array */ public function __invoke(array $record): array { @@ -121,10 +126,6 @@ public function __invoke(array $record): array /** * Escape a CSV record. - * - * @param array $record - * - * @return array */ public function escapeRecord(array $record): array { @@ -133,10 +134,6 @@ public function escapeRecord(array $record): array /** * Escape a CSV cell. - * - * @param mixed $cell - * - * @return mixed */ protected function escapeField($cell) { @@ -154,10 +151,6 @@ protected function escapeField($cell) /** * Tell whether the submitted value is stringable. - * - * @param mixed $value - * - * @return bool */ protected function isStringable($value): bool { diff --git a/src/Exception.php b/src/Exception.php index 4c388275..0ab0a675 100644 --- a/src/Exception.php +++ b/src/Exception.php @@ -1,21 +1,23 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + declare(strict_types=1); namespace League\Csv; /** - * League Csv Base Exception + * League Csv Base Exception. * * @package League.csv * @since 9.0.0 diff --git a/src/HTMLConverter.php b/src/HTMLConverter.php index 32e35e8b..5f58fc04 100644 --- a/src/HTMLConverter.php +++ b/src/HTMLConverter.php @@ -1,24 +1,27 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + declare(strict_types=1); namespace League\Csv; use DOMException; use Traversable; +use function preg_match; /** - * A class to convert tabular data into an HTML Table string + * A class to convert tabular data into an HTML Table string. * * @package League.csv * @since 9.0.0 @@ -27,14 +30,14 @@ class HTMLConverter { /** - * table class attribute value + * table class attribute value. * * @var string */ protected $class_name = 'table-csv-data'; /** - * table id attribute value + * table id attribute value. * * @var string */ @@ -46,7 +49,7 @@ class HTMLConverter protected $xml_converter; /** - * New Instance + * New Instance. */ public function __construct() { @@ -58,11 +61,9 @@ public function __construct() } /** - * Convert an Record collection into a DOMDocument + * Convert an Record collection into a DOMDocument. * * @param array|Traversable $records the tabular data collection - * - * @return string */ public function convert($records): string { @@ -74,14 +75,9 @@ public function convert($records): string } /** - * HTML table class name setter - * - * @param string $class_name - * @param string $id_value + * HTML table class name setter. * * @throws DOMException if the id_value contains any type of whitespace - * - * @return self */ public function table(string $class_name, string $id_value = ''): self { @@ -96,11 +92,7 @@ public function table(string $class_name, string $id_value = ''): self } /** - * HTML tr record offset attribute setter - * - * @param string $record_offset_attribute_name - * - * @return self + * HTML tr record offset attribute setter. */ public function tr(string $record_offset_attribute_name): self { @@ -111,11 +103,7 @@ public function tr(string $record_offset_attribute_name): self } /** - * HTML td field name attribute setter - * - * @param string $fieldname_attribute_name - * - * @return self + * HTML td field name attribute setter. */ public function td(string $fieldname_attribute_name): self { diff --git a/src/MapIterator.php b/src/MapIterator.php index 7ff601e3..2b990881 100644 --- a/src/MapIterator.php +++ b/src/MapIterator.php @@ -1,15 +1,17 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + declare(strict_types=1); namespace League\Csv; @@ -18,7 +20,7 @@ use Traversable; /** - * Map value from an iterator before yielding + * Map value from an iterator before yielding. * * @package League.csv * @since 3.3.0 @@ -28,17 +30,14 @@ class MapIterator extends IteratorIterator { /** - * The callback to apply on all InnerIterator current value + * The callback to apply on all InnerIterator current value. * * @var callable */ protected $callable; /** - * The Constructor - * - * @param Traversable $iterator - * @param callable $callable + * New instance. */ public function __construct(Traversable $iterator, callable $callable) { diff --git a/src/RFC4180Field.php b/src/RFC4180Field.php index 7b9e8abc..6f5f8ada 100644 --- a/src/RFC4180Field.php +++ b/src/RFC4180Field.php @@ -1,24 +1,38 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + declare(strict_types=1); namespace League\Csv; use InvalidArgumentException; use php_user_filter; +use const STREAM_FILTER_READ; +use const STREAM_FILTER_WRITE; +use function array_map; +use function in_array; +use function is_string; +use function str_replace; +use function strcspn; +use function stream_bucket_append; +use function stream_bucket_make_writeable; +use function stream_filter_register; +use function stream_get_filters; +use function strlen; /** - * A stream filter to conform the CSV field to RFC4180 + * A stream filter to conform the CSV field to RFC4180. * * @see https://tools.ietf.org/html/rfc4180#section-2 * @@ -31,7 +45,7 @@ class RFC4180Field extends php_user_filter const FILTERNAME = 'convert.league.csv.rfc4180'; /** - * the filter name used to instantiate the class with + * the filter name used to instantiate the class with. * * @var string */ @@ -39,40 +53,35 @@ class RFC4180Field extends php_user_filter /** * Contents of the params parameter passed to stream_filter_append - * or stream_filter_prepend functions + * or stream_filter_prepend functions. * * @var mixed */ public $params; /** - * The value being search for + * The value being search for. * * @var string[] */ protected $search; /** - * The replacement value that replace found $search values + * The replacement value that replace found $search values. * * @var string[] */ protected $replace; /** - * Characters that triggers enclosure with PHP fputcsv + * Characters that triggers enclosure with PHP fputcsv. * * @var string */ protected static $force_enclosure = "\n\r\t "; /** - * Static method to add the stream filter to a {@link AbstractCsv} object - * - * @param AbstractCsv $csv - * @param string $whitespace_replace - * - * @return AbstractCsv + * Static method to add the stream filter to a {@link AbstractCsv} object. */ public static function addTo(AbstractCsv $csv, string $whitespace_replace = ''): AbstractCsv { @@ -94,12 +103,7 @@ public static function addTo(AbstractCsv $csv, string $whitespace_replace = ''): /** * Add a formatter to the {@link Writer} object to format the record - * field to avoid enclosure around a field with an empty space - * - * @param Writer $csv - * @param string $whitespace_replace - * - * @return Writer + * field to avoid enclosure around a field with an empty space. */ public static function addFormatterTo(Writer $csv, string $whitespace_replace): Writer { @@ -123,7 +127,7 @@ public static function addFormatterTo(Writer $csv, string $whitespace_replace): } /** - * Static method to register the class as a stream filter + * Static method to register the class as a stream filter. */ public static function register() { @@ -133,9 +137,7 @@ public static function register() } /** - * Static method to return the stream filter filtername - * - * @return string + * Static method to return the stream filter filtername. */ public static function getFiltername(): string { @@ -182,11 +184,7 @@ public function onCreate() } /** - * Validate params property - * - * @param array $params - * - * @return bool + * Validate params property. */ protected function isValidParams(array $params): bool { @@ -198,9 +196,7 @@ protected function isValidParams(array $params): bool } /** - * Is Valid White space replaced sequence - * - * @param array $params + * Is Valid White space replaced sequence. * * @return bool */ diff --git a/src/Reader.php b/src/Reader.php index 58720b1b..a6a85df7 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -1,15 +1,17 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + declare(strict_types=1); namespace League\Csv; @@ -22,9 +24,24 @@ use JsonSerializable; use SplFileObject; use TypeError; +use const STREAM_FILTER_READ; +use function array_combine; +use function array_filter; +use function array_pad; +use function array_slice; +use function array_unique; +use function gettype; +use function is_array; +use function iterator_count; +use function iterator_to_array; +use function mb_strlen; +use function mb_substr; +use function sprintf; +use function strlen; +use function substr; /** - * A class to select records from a CSV document + * A class to select records from a CSV document. * * @package League.csv * @since 3.0.0 @@ -36,21 +53,21 @@ class Reader extends AbstractCsv implements Countable, IteratorAggregate, JsonSerializable { /** - * header offset + * header offset. * * @var int|null */ protected $header_offset; /** - * header record + * header record. * * @var string[] */ protected $header = []; /** - * records count + * records count. * * @var int */ @@ -70,7 +87,7 @@ public static function createFromPath(string $path, string $open_mode = 'r', $co } /** - * Returns the header offset + * Returns the header offset. * * If no CSV header offset is set this method MUST return null * @@ -82,7 +99,7 @@ public function getHeaderOffset() } /** - * Returns the CSV record used as header + * Returns the CSV record used as header. * * The returned header is represented as an array of string values * @@ -94,7 +111,7 @@ public function getHeader(): array return $this->header; } - if (!empty($this->header)) { + if ([] !== $this->header) { return $this->header; } @@ -104,9 +121,7 @@ public function getHeader(): array } /** - * Determine the CSV record header - * - * @param int $offset + * Determine the CSV record header. * * @throws Exception If the header offset is set and no record is found or is the empty array * @@ -115,7 +130,7 @@ public function getHeader(): array protected function setHeader(int $offset): array { $header = $this->seekRow($offset); - if (empty($header)) { + if (false === $header || [] === $header) { throw new Exception(sprintf('The header record does not exist or is empty at offset: `%s`', $offset)); } @@ -127,11 +142,7 @@ protected function setHeader(int $offset): array } /** - * Returns the row at a given offset - * - * @param int $offset - * - * @return mixed + * Returns the row at a given offset. */ protected function seekRow(int $offset) { @@ -154,11 +165,9 @@ protected function seekRow(int $offset) } /** - * Strip the BOM sequence from a record + * Strip the BOM sequence from a record. * * @param string[] $record - * @param int $bom_length - * @param string $enclosure * * @return string[] */ @@ -232,8 +241,6 @@ public function jsonSerialize(): array * the returned object. * * @param string[] $header an optional header to use instead of the CSV document header - * - * @return Iterator */ public function getRecords(array $header = []): Iterator { @@ -256,7 +263,7 @@ public function getRecords(array $header = []): Iterator } /** - * Returns the header to be used for iteration + * Returns the header to be used for iteration. * * @param string[] $header * @@ -266,7 +273,7 @@ public function getRecords(array $header = []): Iterator */ protected function computeHeader(array $header) { - if (empty($header)) { + if ([] === $header) { $header = $this->getHeader(); } @@ -278,16 +285,13 @@ protected function computeHeader(array $header) } /** - * Combine the CSV header to each record if present + * Combine the CSV header to each record if present. * - * @param Iterator $iterator * @param string[] $header - * - * @return Iterator */ protected function combineHeader(Iterator $iterator, array $header): Iterator { - if (empty($header)) { + if ([] === $header) { return $iterator; } @@ -304,12 +308,7 @@ protected function combineHeader(Iterator $iterator, array $header): Iterator } /** - * Strip the BOM sequence from the returned records if necessary - * - * @param Iterator $iterator - * @param string $bom - * - * @return Iterator + * Strip the BOM sequence from the returned records if necessary. */ protected function stripBOM(Iterator $iterator, string $bom): Iterator { @@ -330,7 +329,7 @@ protected function stripBOM(Iterator $iterator, string $bom): Iterator } /** - * Selects the record to be used as the CSV header + * Selects the record to be used as the CSV header. * * Because the header is represented as an array, to be valid * a header MUST contain only unique string value. diff --git a/src/ResultSet.php b/src/ResultSet.php index fa2cbb99..909332d6 100644 --- a/src/ResultSet.php +++ b/src/ResultSet.php @@ -1,15 +1,17 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + declare(strict_types=1); namespace League\Csv; @@ -21,9 +23,15 @@ use IteratorAggregate; use JsonSerializable; use LimitIterator; +use function array_flip; +use function array_search; +use function is_string; +use function iterator_count; +use function iterator_to_array; +use function sprintf; /** - * Represents the result set of a {@link Reader} processed by a {@link Statement} + * Represents the result set of a {@link Reader} processed by a {@link Statement}. * * @package League.csv * @since 9.0.0 @@ -32,21 +40,21 @@ class ResultSet implements Countable, IteratorAggregate, JsonSerializable { /** - * The CSV records collection + * The CSV records collection. * * @var Iterator */ protected $records; /** - * The CSV records collection header + * The CSV records collection header. * * @var array */ protected $header = []; /** - * New instance + * New instance. * * @param Iterator $records a CSV records collection iterator * @param array $header the associated collection column names @@ -66,7 +74,7 @@ public function __destruct() } /** - * Returns the header associated with the result set + * Returns the header associated with the result set. * * @return string[] */ @@ -110,15 +118,13 @@ public function jsonSerialize(): array } /** - * Returns the nth record from the result set + * Returns the nth record from the result set. * * By default if no index is provided the first record of the resultet is returned * * @param int $nth_record the CSV record offset * * @throws Exception if argument is lesser than 0 - * - * @return array */ public function fetchOne(int $nth_record = 0): array { @@ -133,13 +139,11 @@ public function fetchOne(int $nth_record = 0): array } /** - * Returns a single column from the next record of the result set + * Returns a single column from the next record of the result set. * * By default if no value is supplied the first column is fetch * * @param string|int $index CSV column index - * - * @return Generator */ public function fetchColumn($index = 0): Generator { @@ -159,7 +163,7 @@ public function fetchColumn($index = 0): Generator } /** - * Filter a column name against the header if any + * Filter a column name against the header if any. * * @param string|int $field the field name or the field index * @param string $error_message the associated error message @@ -174,14 +178,9 @@ protected function getColumnIndex($field, string $error_message) } /** - * Returns the selected column name - * - * @param string $value - * @param string $error_message + * Returns the selected column name. * * @throws Exception if the column is not found - * - * @return string */ protected function getColumnIndexByValue(string $value, string $error_message): string { @@ -193,10 +192,7 @@ protected function getColumnIndexByValue(string $value, string $error_message): } /** - * Returns the selected column name according to its offset - * - * @param int $index - * @param string $error_message + * Returns the selected column name according to its offset. * * @throws Exception if the field is invalid or not found * @@ -208,7 +204,7 @@ protected function getColumnIndexByKey(int $index, string $error_message) throw new Exception($error_message); } - if (empty($this->header)) { + if ([] === $this->header) { return $index; } @@ -230,8 +226,6 @@ protected function getColumnIndexByKey(int $index, string $error_message) * * @param string|int $offset_index The column index to serve as offset * @param string|int $value_index The column index to serve as value - * - * @return Generator */ public function fetchPairs($offset_index = 0, $value_index = 1): Generator { diff --git a/src/Statement.php b/src/Statement.php index b436e445..d4fab335 100644 --- a/src/Statement.php +++ b/src/Statement.php @@ -1,15 +1,17 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + declare(strict_types=1); namespace League\Csv; @@ -18,9 +20,11 @@ use CallbackFilterIterator; use Iterator; use LimitIterator; +use function array_reduce; +use function iterator_to_array; /** - * A Prepared statement to be executed on a {@link Reader} object + * A Prepared statement to be executed on a {@link Reader} object. * * @package League.csv * @since 9.0.0 @@ -29,39 +33,35 @@ class Statement { /** - * Callables to filter the iterator + * Callables to filter the iterator. * * @var callable[] */ protected $where = []; /** - * Callables to sort the iterator + * Callables to sort the iterator. * * @var callable[] */ protected $order_by = []; /** - * iterator Offset + * iterator Offset. * * @var int */ protected $offset = 0; /** - * iterator maximum length + * iterator maximum length. * * @var int */ protected $limit = -1; /** - * Set the Iterator filter method - * - * @param callable $callable - * - * @return self + * Set the Iterator filter method. */ public function where(callable $callable): self { @@ -72,11 +72,7 @@ public function where(callable $callable): self } /** - * Set an Iterator sorting callable function - * - * @param callable $callable - * - * @return self + * Set an Iterator sorting callable function. */ public function orderBy(callable $callable): self { @@ -87,13 +83,9 @@ public function orderBy(callable $callable): self } /** - * Set LimitIterator Offset - * - * @param int $offset + * Set LimitIterator Offset. * * @throws Exception if the offset is lesser than 0 - * - * @return self */ public function offset(int $offset): self { @@ -112,13 +104,9 @@ public function offset(int $offset): self } /** - * Set LimitIterator Count - * - * @param int $limit + * Set LimitIterator Count. * * @throws Exception if the limit is lesser than -1 - * - * @return self */ public function limit(int $limit): self { @@ -137,16 +125,13 @@ public function limit(int $limit): self } /** - * Execute the prepared Statement on the {@link Reader} object + * Execute the prepared Statement on the {@link Reader} object. * - * @param Reader $csv * @param string[] $header an optional header to use instead of the CSV document header - * - * @return ResultSet */ public function process(Reader $csv, array $header = []): ResultSet { - if (empty($header)) { + if ([] === $header) { $header = $csv->getHeader(); } @@ -157,12 +142,7 @@ public function process(Reader $csv, array $header = []): ResultSet } /** - * Filters elements of an Iterator using a callback function - * - * @param Iterator $iterator - * @param callable $callable - * - * @return CallbackFilterIterator + * Filters elements of an Iterator using a callback function. */ protected function filter(Iterator $iterator, callable $callable): CallbackFilterIterator { @@ -170,15 +150,11 @@ protected function filter(Iterator $iterator, callable $callable): CallbackFilte } /** - * Sort the Iterator - * - * @param Iterator $iterator - * - * @return Iterator - */ + * Sort the Iterator. + */ protected function buildOrderBy(Iterator $iterator): Iterator { - if (empty($this->order_by)) { + if ([] === $this->order_by) { return $iterator; } diff --git a/src/Stream.php b/src/Stream.php index 6396850d..f708cc27 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -1,15 +1,17 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + declare(strict_types=1); namespace League\Csv; @@ -18,6 +20,30 @@ use SplFileObject; use TypeError; +use function array_keys; +use function array_values; +use function array_walk_recursive; +use function fclose; +use function feof; +use function fflush; +use function fgetcsv; +use function fopen; +use function fpassthru; +use function fputcsv; +use function fread; +use function fseek; +use function fwrite; +use function get_class; +use function get_resource_type; +use function gettype; +use function is_resource; +use function rewind; +use function sprintf; +use function stream_filter_append; +use function stream_filter_remove; +use function stream_get_meta_data; +use function strlen; + /** * An object oriented API for a CSV stream resource. * @@ -29,77 +55,77 @@ class Stream implements SeekableIterator { /** - * Attached filters + * Attached filters. * * @var resource[] */ protected $filters = []; /** - * stream resource + * stream resource. * * @var resource */ protected $stream; /** - * Tell whether the stream should be closed on object destruction + * Tell whether the stream should be closed on object destruction. * * @var bool */ protected $should_close_stream = false; /** - * Current iterator value + * Current iterator value. * * @var mixed */ protected $value; /** - * Current iterator key + * Current iterator key. * * @var int */ protected $offset; /** - * Flags for the Document + * Flags for the Document. * * @var int */ protected $flags = 0; /** - * the field delimiter (one character only) + * the field delimiter (one character only). * * @var string */ protected $delimiter = ','; /** - * the field enclosure character (one character only) + * the field enclosure character (one character only). * * @var string */ protected $enclosure = '"'; /** - * the field escape character (one character only) + * the field escape character (one character only). * * @var string */ protected $escape = '\\'; /** - * Tell whether the current stream is seekable; + * Tell whether the current stream is seekable;. * * @var bool */ protected $is_seekable = false; /** - * New instance + * New instance. * * @param resource $resource stream type resource */ @@ -157,11 +183,9 @@ public function __debugInfo() } /** - * Return a new instance from a file path + * Return a new instance from a file path. * - * @param string $path file path - * @param string $open_mode the file open mode flag - * @param resource|null $context the resource context + * @param resource|null $context * * @throws Exception if the stream resource can not be created * @@ -186,9 +210,8 @@ public static function createFromPath(string $path, string $open_mode = 'r', $co } /** - * Return a new instance from a string + * Return a new instance from a string. * - * @param string $content the CSV document as a string * * @return static */ @@ -204,15 +227,12 @@ public static function createFromString(string $content) } /** - * append a filter + * append a filter. * * @see http://php.net/manual/en/function.stream-filter-append.php * - * @param string $filtername - * @param int $read_write - * @param mixed $params - * - * @throws Exception if the filter can not be appended + * @param null|mixed $params + * @throws Exception if the filter can not be appended */ public function appendFilter(string $filtername, int $read_write, $params = null) { @@ -226,13 +246,9 @@ public function appendFilter(string $filtername, int $read_write, $params = null } /** - * Set CSV control + * Set CSV control. * * @see http://php.net/manual/en/splfileobject.setcsvcontrol.php - * - * @param string $delimiter - * @param string $enclosure - * @param string $escape */ public function setCsvControl(string $delimiter = ',', string $enclosure = '"', string $escape = '\\') { @@ -240,16 +256,10 @@ public function setCsvControl(string $delimiter = ',', string $enclosure = '"', } /** - * Filter Csv control characters + * Filter Csv control characters. * - * @param string $delimiter CSV delimiter character - * @param string $enclosure CSV enclosure character - * @param string $escape CSV escape character - * @param string $caller caller * * @throws Exception If the Csv control character is not one character only. - * - * @return array */ protected function filterControl(string $delimiter, string $enclosure, string $escape, string $caller): array { @@ -264,7 +274,7 @@ protected function filterControl(string $delimiter, string $enclosure, string $e } /** - * Set CSV control + * Set CSV control. * * @see http://php.net/manual/en/splfileobject.getcsvcontrol.php * @@ -276,11 +286,9 @@ public function getCsvControl() } /** - * Set CSV stream flags + * Set CSV stream flags. * * @see http://php.net/manual/en/splfileobject.setflags.php - * - * @param int $flags */ public function setFlags(int $flags) { @@ -288,15 +296,10 @@ public function setFlags(int $flags) } /** - * Write a field array as a CSV line + * Write a field array as a CSV line. * * @see http://php.net/manual/en/splfileobject.fputcsv.php * - * @param array $fields - * @param string $delimiter - * @param string $enclosure - * @param string $escape - * * @return int|null|bool */ public function fputcsv(array $fields, string $delimiter = ',', string $enclosure = '"', string $escape = '\\') @@ -307,7 +310,7 @@ public function fputcsv(array $fields, string $delimiter = ',', string $enclosur } /** - * Get line number + * Get line number. * * @see http://php.net/manual/en/splfileobject.key.php * @@ -319,10 +322,9 @@ public function key() } /** - * Read next line + * Read next line. * * @see http://php.net/manual/en/splfileobject.next.php - * */ public function next() { @@ -331,7 +333,7 @@ public function next() } /** - * Rewind the file to the first line + * Rewind the file to the first line. * * @see http://php.net/manual/en/splfileobject.rewind.php * @@ -352,7 +354,7 @@ public function rewind() } /** - * Not at EOF + * Not at EOF. * * @see http://php.net/manual/en/splfileobject.valid.php * @@ -371,8 +373,6 @@ public function valid() * Retrieves the current line of the file. * * @see http://php.net/manual/en/splfileobject.current.php - * - * @return mixed */ public function current() { @@ -386,7 +386,7 @@ public function current() } /** - * Retrieves the current line as a CSV Record + * Retrieves the current line as a CSV Record. * * @return array|bool */ @@ -400,7 +400,7 @@ protected function getCurrentRecord() } /** - * Seek to specified line + * Seek to specified line. * * @see http://php.net/manual/en/splfileobject.seek.php * @@ -424,7 +424,7 @@ public function seek($position) } /** - * Output all remaining data on a file pointer + * Output all remaining data on a file pointer. * * @see http://php.net/manual/en/splfileobject.fpatssthru.php * @@ -436,7 +436,7 @@ public function fpassthru() } /** - * Read from file + * Read from file. * * @see http://php.net/manual/en/splfileobject.fread.php * @@ -450,12 +450,10 @@ public function fread($length) } /** - * Seek to a position + * Seek to a position. * * @see http://php.net/manual/en/splfileobject.fseek.php * - * @param int $offset - * @param int $whence * * @throws Exception if the stream resource is not seekable * @@ -471,13 +469,10 @@ public function fseek(int $offset, int $whence = SEEK_SET) } /** - * Write to stream + * Write to stream. * * @see http://php.net/manual/en/splfileobject.fwrite.php * - * @param string $str - * @param int $length - * * @return int|bool */ public function fwrite(string $str, int $length = 0) @@ -486,7 +481,7 @@ public function fwrite(string $str, int $length = 0) } /** - * Flushes the output to a file + * Flushes the output to a file. * * @see http://php.net/manual/en/splfileobject.fwrite.php * diff --git a/src/Writer.php b/src/Writer.php index aa3edfb9..9033f49c 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -1,24 +1,32 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + declare(strict_types=1); namespace League\Csv; use Traversable; use TypeError; +use const SEEK_CUR; +use const STREAM_FILTER_WRITE; +use function array_reduce; +use function gettype; +use function sprintf; +use function strlen; /** - * A class to insert records into a CSV Document + * A class to insert records into a CSV Document. * * @package League.csv * @since 4.0.0 @@ -27,35 +35,35 @@ class Writer extends AbstractCsv { /** - * callable collection to format the record before insertion + * callable collection to format the record before insertion. * * @var callable[] */ protected $formatters = []; /** - * callable collection to validate the record before insertion + * callable collection to validate the record before insertion. * * @var callable[] */ protected $validators = []; /** - * newline character + * newline character. * * @var string */ protected $newline = "\n"; /** - * Insert records count for flushing + * Insert records count for flushing. * * @var int */ protected $flush_counter = 0; /** - * Buffer flush threshold + * Buffer flush threshold. * * @var int|null */ @@ -67,9 +75,7 @@ class Writer extends AbstractCsv protected $stream_filter_mode = STREAM_FILTER_WRITE; /** - * Returns the current newline sequence characters - * - * @return string + * Returns the current newline sequence characters. */ public function getNewline(): string { @@ -77,7 +83,7 @@ public function getNewline(): string } /** - * Get the flush threshold + * Get the flush threshold. * * @return int|null */ @@ -87,13 +93,11 @@ public function getFlushThreshold() } /** - * Adds multiple records to the CSV document + * Adds multiple records to the CSV document. * * @see Writer::insertOne * - * @param Traversable|array $records a multidimensional array or a Traversable object - * - * @return int + * @param Traversable|array $records */ public function insertAll($records): int { @@ -113,16 +117,13 @@ public function insertAll($records): int } /** - * Adds a single record to a CSV document + * Adds a single record to a CSV document. * - * @param array $record An array containing - * - scalar types values, - * - NULL values, - * - or objects implementing the __toString() method. + * A record is an array that can contains scalar types values, NULL values + * or objects implementing the __toString method. * - * @throws CannotInsertRecord If the record can not be inserted * - * @return int + * @throws CannotInsertRecord If the record can not be inserted */ public function insertOne(array $record): int { @@ -137,21 +138,13 @@ public function insertOne(array $record): int } /** - * Format a record + * Format a record. * * The returned array must contain * - scalar types values, * - NULL values, * - or objects implementing the __toString() method. * - * @param array $record An array containing - * - scalar types values, - * - NULL values, - * - implementing the __toString() method. - * - * @param callable $formatter - * - * @return array */ protected function formatRecord(array $record, callable $formatter): array { @@ -159,12 +152,8 @@ protected function formatRecord(array $record, callable $formatter): array } /** - * Validate a record + * Validate a record. * - * @param array $record An array containing - * - scalar types values, - * - NULL values - * - or objects implementing __toString() method. * * @throws CannotInsertRecord If the validation failed */ @@ -178,9 +167,7 @@ protected function validateRecord(array $record) } /** - * Apply post insertion actions - * - * @return int + * Apply post insertion actions. */ protected function consolidate(): int { @@ -204,9 +191,7 @@ protected function consolidate(): int } /** - * Adds a record formatter - * - * @param callable $formatter + * Adds a record formatter. * * @return static */ @@ -218,10 +203,8 @@ public function addFormatter(callable $formatter): self } /** - * Adds a record validator + * Adds a record validator. * - * @param callable $validator - * @param string $validator_name the validator name * * @return static */ @@ -233,9 +216,7 @@ public function addValidator(callable $validator, string $validator_name): self } /** - * Sets the newline sequence - * - * @param string $newline + * Sets the newline sequence. * * @return static */ @@ -247,7 +228,7 @@ public function setNewline(string $newline): self } /** - * Set the flush threshold + * Set the flush threshold. * * @param int|null $threshold * diff --git a/src/XMLConverter.php b/src/XMLConverter.php index 28f3bbfa..5e89e5ce 100644 --- a/src/XMLConverter.php +++ b/src/XMLConverter.php @@ -1,15 +1,17 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + declare(strict_types=1); namespace League\Csv; @@ -20,9 +22,11 @@ use DOMException; use Traversable; use TypeError; +use function gettype; +use function sprintf; /** - * A class to convert tabular data into a DOMDOcument object + * A class to convert tabular data into a DOMDOcument object. * * @package League.csv * @since 9.0.0 @@ -31,42 +35,42 @@ class XMLConverter { /** - * XML Root name + * XML Root name. * * @var string */ protected $root_name = 'csv'; /** - * XML Node name + * XML Node name. * * @var string */ protected $record_name = 'row'; /** - * XML Item name + * XML Item name. * * @var string */ protected $field_name = 'cell'; /** - * XML column attribute name + * XML column attribute name. * * @var string */ protected $column_attr = ''; /** - * XML offset attribute name + * XML offset attribute name. * * @var string */ protected $offset_attr = ''; /** - * Conversion method list + * Conversion method list. * * @var array */ @@ -82,11 +86,9 @@ class XMLConverter ]; /** - * Convert an Record collection into a DOMDocument + * Convert an Record collection into a DOMDocument. * * @param array|Traversable $records the CSV records collection - * - * @return DOMDocument */ public function convert($records): DOMDocument { @@ -109,14 +111,8 @@ public function convert($records): DOMDocument /** * Convert a CSV record into a DOMElement and - * adds its offset as DOMElement attribute - * - * @param DOMDocument $doc - * @param array $record CSV record - * @param string $field_encoder CSV Cell encoder method name - * @param int $offset CSV record offset + * adds its offset as DOMElement attribute. * - * @return DOMElement */ protected function recordToElementWithAttribute( DOMDocument $doc, @@ -131,13 +127,8 @@ protected function recordToElementWithAttribute( } /** - * Convert a CSV record into a DOMElement - * - * @param DOMDocument $doc - * @param array $record CSV record - * @param string $field_encoder CSV Cell encoder method name + * Convert a CSV record into a DOMElement. * - * @return DOMElement */ protected function recordToElement(DOMDocument $doc, array $record, string $field_encoder): DOMElement { @@ -156,11 +147,7 @@ protected function recordToElement(DOMDocument $doc, array $record, string $fiel * Convert the CSV item into a DOMElement and adds the item offset * as attribute to the returned DOMElement * - * @param DOMDocument $doc - * @param string $value Record item value - * @param int|string $node_name Record item offset - * - * @return DOMElement + * @param int|string $node_name */ protected function fieldToElementWithAttribute(DOMDocument $doc, string $value, $node_name): DOMElement { @@ -171,12 +158,9 @@ protected function fieldToElementWithAttribute(DOMDocument $doc, string $value, } /** - * Convert Cell to Item - * - * @param DOMDocument $doc - * @param string $value Record item value + * Convert Cell to Item. * - * @return DOMElement + * @param string $value Record item value */ protected function fieldToElement(DOMDocument $doc, string $value): DOMElement { @@ -187,11 +171,7 @@ protected function fieldToElement(DOMDocument $doc, string $value): DOMElement } /** - * XML root element setter - * - * @param string $node_name - * - * @return self + * XML root element setter. */ public function rootElement(string $node_name): self { @@ -202,13 +182,10 @@ public function rootElement(string $node_name): self } /** - * Filter XML element name + * Filter XML element name. * - * @param string $value Element name * * @throws DOMException If the Element name is invalid - * - * @return string */ protected function filterElementName(string $value): string { @@ -216,12 +193,7 @@ protected function filterElementName(string $value): string } /** - * XML Record element setter - * - * @param string $node_name - * @param string $record_offset_attribute_name - * - * @return self + * XML Record element setter. */ public function recordElement(string $node_name, string $record_offset_attribute_name = ''): self { @@ -233,13 +205,11 @@ public function recordElement(string $node_name, string $record_offset_attribute } /** - * Filter XML attribute name + * Filter XML attribute name. * * @param string $value Element name * * @throws DOMException If the Element attribute name is invalid - * - * @return string */ protected function filterAttributeName(string $value): string { @@ -251,12 +221,7 @@ protected function filterAttributeName(string $value): string } /** - * XML Field element setter - * - * @param string $node_name - * @param string $fieldname_attribute_name - * - * @return self + * XML Field element setter. */ public function fieldElement(string $node_name, string $fieldname_attribute_name = ''): self { diff --git a/src/functions.php b/src/functions.php index d3dd59e7..6dce6ba5 100644 --- a/src/functions.php +++ b/src/functions.php @@ -1,30 +1,36 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + declare(strict_types=1); namespace League\Csv { use ReflectionClass; use Traversable; + use function array_fill_keys; + use function array_filter; + use function array_reduce; + use function array_unique; + use function count; + use function is_array; + use function iterator_to_array; + use function strpos; /** - * Returns the BOM sequence found at the start of the string + * Returns the BOM sequence found at the start of the string. * * If no valid BOM sequence is found an empty string is returned - * - * @param string $str - * - * @return string */ function bom_match(string $str): string { @@ -42,15 +48,13 @@ function bom_match(string $str): string } /** - * Detect Delimiters usage in a {@link Reader} object + * Detect Delimiters usage in a {@link Reader} object. * * Returns a associative array where each key represents * a submitted delimiter and each value the number CSV fields found * when processing at most $limit CSV records with the given delimiter * - * @param Reader $csv the CSV object - * @param string[] $delimiters list of delimiters to consider - * @param int $limit Detection is made using up to $limit records + * @param string[] $delimiters * * @return int[] */ @@ -77,13 +81,9 @@ function delimiter_detect(Reader $csv, array $delimiters, int $limit = 1): array } /** - * Tell whether the content of the variable is iterable + * Tell whether the content of the variable is iterable. * * @see http://php.net/manual/en/function.is-iterable.php - * - * @param mixed $iterable - * - * @return bool */ function is_iterable($iterable): bool { @@ -91,13 +91,9 @@ function is_iterable($iterable): bool } /** - * Tell whether the content of the variable is an int or null + * Tell whether the content of the variable is an int or null. * * @see https://wiki.php.net/rfc/nullable_types - * - * @param mixed $value - * - * @return bool */ function is_nullable_int($value): bool { diff --git a/src/functions_include.php b/src/functions_include.php index bedbf8c3..fd10f35a 100644 --- a/src/functions_include.php +++ b/src/functions_include.php @@ -1,6 +1,16 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ if (!function_exists('League\Csv\bom_match')) { require __DIR__.'/functions.php'; } diff --git a/tests/ByteSequenceTest.php b/tests/ByteSequenceTest.php index 1b23f1a6..4fa1b4ef 100644 --- a/tests/ByteSequenceTest.php +++ b/tests/ByteSequenceTest.php @@ -1,9 +1,22 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace LeagueTest\Csv; use League\Csv\ByteSequence; use PHPUnit\Framework\TestCase; +use function chr; use function League\Csv\bom_match; /** diff --git a/tests/CharsetConverterTest.php b/tests/CharsetConverterTest.php index 6f4f8e48..29ce2bff 100644 --- a/tests/CharsetConverterTest.php +++ b/tests/CharsetConverterTest.php @@ -1,5 +1,17 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace LeagueTest\Csv; use ArrayIterator; @@ -10,6 +22,12 @@ use OutOfRangeException; use PHPUnit\Framework\TestCase; use TypeError; +use function explode; +use function implode; +use function mb_convert_encoding; +use function stream_filter_register; +use function stream_get_filters; +use function strtoupper; /** * @group converter @@ -156,8 +174,6 @@ public function testOnCreateFailedWithWrongParams() * @covers ::encodeField * * @dataProvider converterProvider - * @param array $record - * @param array $expected */ public function testConvertOnlyStringField(array $record, array $expected) { diff --git a/tests/ColumnConsistencyTest.php b/tests/ColumnConsistencyTest.php index be5ca233..3511494e 100644 --- a/tests/ColumnConsistencyTest.php +++ b/tests/ColumnConsistencyTest.php @@ -1,5 +1,17 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace LeagueTest\Csv; use League\Csv\CannotInsertRecord; diff --git a/tests/CsvTest.php b/tests/CsvTest.php index 9e72c687..73d2b24c 100644 --- a/tests/CsvTest.php +++ b/tests/CsvTest.php @@ -1,5 +1,17 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace LeagueTest\Csv; use League\Csv\Exception; @@ -7,6 +19,17 @@ use League\Csv\Writer; use PHPUnit\Framework\TestCase; use SplTempFileObject; +use const PHP_EOL; +use const PHP_VERSION; +use const STREAM_FILTER_READ; +use const STREAM_FILTER_WRITE; +use function chr; +use function function_exists; +use function iterator_to_array; +use function strtolower; +use function tmpfile; +use function unlink; +use function version_compare; /** * @group csv diff --git a/tests/DetectDelimiterTest.php b/tests/DetectDelimiterTest.php index 4f752ae2..42126d50 100644 --- a/tests/DetectDelimiterTest.php +++ b/tests/DetectDelimiterTest.php @@ -1,5 +1,17 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace LeagueTest\Csv; use League\Csv\Exception; diff --git a/tests/EncloseFieldTest.php b/tests/EncloseFieldTest.php index 984e4baa..9917e757 100644 --- a/tests/EncloseFieldTest.php +++ b/tests/EncloseFieldTest.php @@ -1,11 +1,24 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace LeagueTest\Csv; use InvalidArgumentException; use League\Csv\EncloseField; use League\Csv\Writer; use PHPUnit\Framework\TestCase; +use function stream_get_filters; /** * @group filter @@ -49,7 +62,6 @@ public function testEncloseAll() * @covers ::onCreate * @covers ::isValidSequence * @dataProvider wrongParamProvider - * @param array $params */ public function testOnCreateFailedWithWrongParams(array $params) { diff --git a/tests/EscapeFormulaTest.php b/tests/EscapeFormulaTest.php index 0de5d5ae..7109cb3c 100644 --- a/tests/EscapeFormulaTest.php +++ b/tests/EscapeFormulaTest.php @@ -1,5 +1,17 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace LeagueTest\Csv; use InvalidArgumentException; diff --git a/tests/HTMLConverterTest.php b/tests/HTMLConverterTest.php index 4fd7e64d..f7053f9e 100644 --- a/tests/HTMLConverterTest.php +++ b/tests/HTMLConverterTest.php @@ -1,5 +1,17 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace LeagueTest\Csv; use DOMException; diff --git a/tests/RFC4180FieldTest.php b/tests/RFC4180FieldTest.php index c958a38e..5465691b 100644 --- a/tests/RFC4180FieldTest.php +++ b/tests/RFC4180FieldTest.php @@ -1,5 +1,17 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace LeagueTest\Csv; use InvalidArgumentException; @@ -8,6 +20,9 @@ use League\Csv\Writer; use PHPUnit\Framework\TestCase; use TypeError; +use const STREAM_FILTER_ALL; +use const STREAM_FILTER_READ; +use function stream_get_filters; /** * @group filter @@ -29,7 +44,6 @@ class RFC4180FieldTest extends TestCase * @dataProvider bugsProvider * * @param string $expected - * @param array $record */ public function testStreamFilterOnWrite($expected, array $record) { @@ -68,7 +82,6 @@ public function bugsProvider() * @dataProvider readerBugsProvider * * @param string $expected - * @param array $record */ public function testStreamFilterOnRead($expected, array $record) { @@ -101,7 +114,6 @@ public function testOnCreateFailedWithoutParams() * @covers ::onCreate * @covers ::isValidParams * @dataProvider wrongParamProvider - * @param array $params */ public function testOnCreateFailedWithWrongParams(array $params) { diff --git a/tests/ReaderTest.php b/tests/ReaderTest.php index 2f7ea7b4..f833c918 100644 --- a/tests/ReaderTest.php +++ b/tests/ReaderTest.php @@ -1,5 +1,17 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace LeagueTest\Csv; use BadMethodCallException; @@ -10,6 +22,15 @@ use SplFileObject; use SplTempFileObject; use TypeError; +use function array_keys; +use function count; +use function fclose; +use function fopen; +use function fputcsv; +use function in_array; +use function iterator_to_array; +use function json_encode; +use function unlink; /** * @group reader @@ -180,9 +201,6 @@ public function testHeaderThrowsExceptionOnError() * @covers ::combineHeader * @covers League\Csv\Stream * @dataProvider validBOMSequences - * @param array $record - * @param string $expected_bom - * @param string $expected */ public function testStripBOM(array $record, string $expected_bom, string $expected) { @@ -258,8 +276,6 @@ public function testStripNoBOM() /** * @covers ::getIterator * @dataProvider appliedFlagsProvider - * @param int $flag - * @param int $fetch_count */ public function testAppliedFlags(int $flag, int $fetch_count) { diff --git a/tests/ResultSetTest.php b/tests/ResultSetTest.php index a1b88f01..5d4e8346 100644 --- a/tests/ResultSetTest.php +++ b/tests/ResultSetTest.php @@ -1,5 +1,17 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace LeagueTest\Csv; use Generator; @@ -9,6 +21,14 @@ use OutOfBoundsException; use PHPUnit\Framework\TestCase; use SplTempFileObject; +use function array_reverse; +use function current; +use function in_array; +use function iterator_to_array; +use function json_encode; +use function next; +use function strcmp; +use function strlen; /** * @group reader @@ -112,8 +132,6 @@ public function testSetOffset() * @covers League\Csv\Statement::offset * @covers League\Csv\Statement::process * @dataProvider intervalTest - * @param int $offset - * @param int $limit */ public function testInterval(int $offset, int $limit) { @@ -364,7 +382,6 @@ public function testFetchOneTriggersException() * @dataProvider fetchPairsDataProvider * @param int|string $key * @param int|string $value - * @param array $expected */ public function testFetchPairsIteratorMode($key, $value, array $expected) { diff --git a/tests/StreamTest.php b/tests/StreamTest.php index 59d54027..db18f7d6 100644 --- a/tests/StreamTest.php +++ b/tests/StreamTest.php @@ -1,5 +1,17 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace LeagueTest\Csv; use League\Csv\Exception; @@ -7,6 +19,13 @@ use PHPUnit\Framework\TestCase; use SplFileObject; use TypeError; +use const STREAM_FILTER_READ; +use function curl_init; +use function fopen; +use function fputcsv; +use function stream_context_create; +use function stream_wrapper_register; +use function stream_wrapper_unregister; /** * @group csv diff --git a/tests/StreamWrapper.php b/tests/StreamWrapper.php index e374f3ba..2fe6d73b 100644 --- a/tests/StreamWrapper.php +++ b/tests/StreamWrapper.php @@ -1,18 +1,28 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ namespace LeagueTest\Csv; +use function feof; +use function fread; +use function fseek; +use function ftell; +use function fwrite; +use function stream_context_get_options; +use function stream_get_wrappers; +use function stream_wrapper_register; + final class StreamWrapper { const PROTOCOL = 'leaguetest'; diff --git a/tests/WriterTest.php b/tests/WriterTest.php index 5d056b06..77393158 100644 --- a/tests/WriterTest.php +++ b/tests/WriterTest.php @@ -1,5 +1,17 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace LeagueTest\Csv; use ArrayIterator; @@ -12,6 +24,10 @@ use stdClass; use Traversable; use TypeError; +use function array_map; +use function fclose; +use function fopen; +use function tmpfile; /** * @group writer @@ -128,7 +144,6 @@ public function testFailedSaveWithWrongType() * @covers ::insertAll * * @param array|Traversable $argument - * @param string $expected * @dataProvider dataToSave */ public function testSave($argument, string $expected) diff --git a/tests/XMLConverterTest.php b/tests/XMLConverterTest.php index ec8f01cd..30dbb8bd 100644 --- a/tests/XMLConverterTest.php +++ b/tests/XMLConverterTest.php @@ -1,5 +1,17 @@ + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace LeagueTest\Csv; use DOMDocument; From 5703ea370489beb8d474b4a2f7a73108a6c3d24f Mon Sep 17 00:00:00 2001 From: Nat Zimmermann Date: Wed, 1 Aug 2018 10:52:47 +0100 Subject: [PATCH 032/858] Use non-deprecated options for ordered_imports PHP-CS-Fixer rule (#304) --- .php_cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.php_cs b/.php_cs index 2c4b840f..9d3bb152 100644 --- a/.php_cs +++ b/.php_cs @@ -36,7 +36,7 @@ return PhpCsFixer\Config::create() 'no_superfluous_phpdoc_tags' => true, 'no_trailing_comma_in_singleline_array' => true, 'no_unused_imports' => true, - 'ordered_imports' => ['importsOrder' => null, 'sortAlgorithm' => 'alpha'], + 'ordered_imports' => ['imports_order' => null, 'sort_algorithm' => 'alpha'], 'phpdoc_add_missing_param_annotation' => ['only_untyped' => false], 'phpdoc_align' => true, 'phpdoc_no_empty_return' => true, @@ -56,4 +56,4 @@ return PhpCsFixer\Config::create() 'whitespace_after_comma_in_array' => true, ]) ->setFinder($finder) -; \ No newline at end of file +; From e8169db498b10c719b0a6f605a3036e99327fb12 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Tue, 7 Aug 2018 08:49:34 +0200 Subject: [PATCH 033/858] Improve Writer::insertOne exception handling --- .travis.yml | 2 +- CHANGELOG.md | 3 +- phpstan.tests.neon | 2 - src/Writer.php | 17 +---- tests/ByteSequenceTest.php | 2 +- tests/CannotInsertRecordTest.php | 55 +++++++++++++++ tests/CharsetConverterTest.php | 32 ++++----- tests/ColumnConsistencyTest.php | 12 ++-- tests/CsvTest.php | 116 ++++++++++++++++++------------- tests/DetectDelimiterTest.php | 12 ++-- tests/EncloseFieldTest.php | 8 +-- tests/EscapeFormulaTest.php | 16 ++--- tests/HTMLConverterTest.php | 8 +-- tests/RFC4180FieldTest.php | 16 ++--- tests/ReaderTest.php | 60 ++++++++-------- tests/ResultSetTest.php | 68 +++++++++--------- tests/StreamTest.php | 34 ++++----- tests/WriterTest.php | 47 ++++++++----- tests/XMLConverterTest.php | 16 ++--- 19 files changed, 297 insertions(+), 229 deletions(-) create mode 100644 tests/CannotInsertRecordTest.php diff --git a/.travis.yml b/.travis.yml index 43ec4a78..f04b472a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ matrix: - php: 7.1 env: COLLECT_COVERAGE=true VALIDATE_CODING_STYLE=true RUN_PHPSTAN=true IGNORE_PLATFORMS=false - php: 7.2 - env: COLLECT_COVERAGE=false VALIDATE_CODING_STYLE=false RUN_PHPSTAN=false IGNORE_PLATFORMS=false + env: COLLECT_COVERAGE=true VALIDATE_CODING_STYLE=false RUN_PHPSTAN=false IGNORE_PLATFORMS=false - php: nightly env: COLLECT_COVERAGE=false VALIDATE_CODING_STYLE=false RUN_PHPSTAN=false IGNORE_PLATFORMS=true allow_failures: diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cc6ee57..ab19bc20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ All Notable changes to `Csv` will be documented in this file ### Fixed -- Nothing +- `Writer::insertOne` fix throwing exception when record can not be inserted ### Removed @@ -33,7 +33,6 @@ All Notable changes to `Csv` will be documented in this file ### Fixed - `Writer::setFlushThreshold` should accept 1 as an argument [#289](https://github.com/thephpleague/csv/issue/289) - - `CharsetConverter::convert` should not try to convert numeric value [#287](https://github.com/thephpleague/csv/issue/287) ### Removed diff --git a/phpstan.tests.neon b/phpstan.tests.neon index 88c8c63d..19af9bb2 100644 --- a/phpstan.tests.neon +++ b/phpstan.tests.neon @@ -1,8 +1,6 @@ includes: - - vendor/phpstan/phpstan-strict-rules/rules.neon - vendor/phpstan/phpstan-phpunit/extension.neon - vendor/phpstan/phpstan-phpunit/rules.neon - - vendor/phpstan/phpstan-phpunit/strictRules.neon parameters: ignoreErrors: - '#Function xdebug_get_headers not found.#' diff --git a/src/Writer.php b/src/Writer.php index 9033f49c..160b2073 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -22,6 +22,7 @@ use const STREAM_FILTER_WRITE; use function array_reduce; use function gettype; +use function is_iterable; use function sprintf; use function strlen; @@ -101,7 +102,7 @@ public function getFlushThreshold() */ public function insertAll($records): int { - if (!\is_iterable($records)) { + if (!is_iterable($records)) { throw new TypeError(sprintf('%s() expects argument passed to be iterable, %s given', __METHOD__, gettype($records))); } @@ -122,7 +123,6 @@ public function insertAll($records): int * A record is an array that can contains scalar types values, NULL values * or objects implementing the __toString method. * - * * @throws CannotInsertRecord If the record can not be inserted */ public function insertOne(array $record): int @@ -130,7 +130,7 @@ public function insertOne(array $record): int $record = array_reduce($this->formatters, [$this, 'formatRecord'], $record); $this->validateRecord($record); $bytes = $this->document->fputcsv($record, $this->delimiter, $this->enclosure, $this->escape); - if ('' !== (string) $bytes) { + if (false !== $bytes && 0 !== $bytes) { return $bytes + $this->consolidate(); } @@ -144,7 +144,6 @@ public function insertOne(array $record): int * - scalar types values, * - NULL values, * - or objects implementing the __toString() method. - * */ protected function formatRecord(array $record, callable $formatter): array { @@ -154,7 +153,6 @@ protected function formatRecord(array $record, callable $formatter): array /** * Validate a record. * - * * @throws CannotInsertRecord If the validation failed */ protected function validateRecord(array $record) @@ -192,8 +190,6 @@ protected function consolidate(): int /** * Adds a record formatter. - * - * @return static */ public function addFormatter(callable $formatter): self { @@ -204,9 +200,6 @@ public function addFormatter(callable $formatter): self /** * Adds a record validator. - * - * - * @return static */ public function addValidator(callable $validator, string $validator_name): self { @@ -217,8 +210,6 @@ public function addValidator(callable $validator, string $validator_name): self /** * Sets the newline sequence. - * - * @return static */ public function setNewline(string $newline): self { @@ -233,8 +224,6 @@ public function setNewline(string $newline): self * @param int|null $threshold * * @throws Exception if the threshold is a integer lesser than 1 - * - * @return static */ public function setFlushThreshold($threshold): self { diff --git a/tests/ByteSequenceTest.php b/tests/ByteSequenceTest.php index 4fa1b4ef..8b2c01e9 100644 --- a/tests/ByteSequenceTest.php +++ b/tests/ByteSequenceTest.php @@ -32,7 +32,7 @@ class ByteSequenceTest extends TestCase */ public function testByteSequenceMatch($str, $expected) { - $this->assertSame($expected, bom_match($str)); + self::assertSame($expected, bom_match($str)); } public function ByteSequenceMatchProvider() diff --git a/tests/CannotInsertRecordTest.php b/tests/CannotInsertRecordTest.php new file mode 100644 index 00000000..c577d317 --- /dev/null +++ b/tests/CannotInsertRecordTest.php @@ -0,0 +1,55 @@ + + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace LeagueTest\Csv; + +use League\Csv\CannotInsertRecord; +use PHPUnit\Framework\TestCase; + +/** + * @group converter + * @coversDefaultClass League\Csv\CannotInsertRecord + */ +class CannotInsertRecordTest extends TestCase +{ + /** + * @covers ::triggerOnInsertion + * @covers ::getName + * @covers ::getRecord + */ + public function testTriggerOnInsertion() + { + $record = ['jane', 'doe', 'jane.doe@example.com']; + $exception = CannotInsertRecord::triggerOnInsertion($record); + + self::assertSame($record, $exception->getRecord()); + self::assertSame('', $exception->getName()); + self::assertSame('Unable to write record to the CSV document', $exception->getMessage()); + } + + /** + * @covers ::triggerOnValidation + * @covers ::getName + * @covers ::getRecord + */ + public function testTriggerOnValidation() + { + $record = ['jane', 'doe', 'jane.doe@example.com']; + $exception = CannotInsertRecord::triggerOnValidation('foo bar', $record); + + self::assertSame($record, $exception->getRecord()); + self::assertSame('foo bar', $exception->getName()); + self::assertSame('Record validation failed', $exception->getMessage()); + } +} diff --git a/tests/CharsetConverterTest.php b/tests/CharsetConverterTest.php index 29ce2bff..2fabea55 100644 --- a/tests/CharsetConverterTest.php +++ b/tests/CharsetConverterTest.php @@ -41,7 +41,7 @@ class CharsetConverterTest extends TestCase */ public function testCharsetConverterTriggersException() { - $this->expectException(OutOfRangeException::class); + self::expectException(OutOfRangeException::class); (new CharsetConverter())->inputEncoding(''); } @@ -50,7 +50,7 @@ public function testCharsetConverterTriggersException() */ public function testCharsetConverterTriggersExceptionOnConversion() { - $this->expectException(TypeError::class); + self::expectException(TypeError::class); (new CharsetConverter())->convert('toto'); } @@ -61,9 +61,9 @@ public function testCharsetConverterTriggersExceptionOnConversion() public function testCharsetConverterRemainsTheSame() { $converter = new CharsetConverter(); - $this->assertSame($converter, $converter->inputEncoding('utf-8')); - $this->assertSame($converter, $converter->outputEncoding('UtF-8')); - $this->assertNotEquals($converter->outputEncoding('iso-8859-15'), $converter); + self::assertSame($converter, $converter->inputEncoding('utf-8')); + self::assertSame($converter, $converter->outputEncoding('UtF-8')); + self::assertNotEquals($converter->outputEncoding('iso-8859-15'), $converter); } /** @@ -78,9 +78,9 @@ public function testCharsetConverterDoesNothing() $converter = new CharsetConverter(); $data = [['a' => 'bé']]; $expected = new ArrayIterator($data); - $this->assertEquals($expected, $converter->convert($expected)); - $this->assertEquals($expected[0], ($converter)($expected[0])); - $this->assertNotEquals($expected[0], ($converter->outputEncoding('utf-16'))($expected[0])); + self::assertEquals($expected, $converter->convert($expected)); + self::assertEquals($expected[0], ($converter)($expected[0])); + self::assertNotEquals($expected[0], ($converter->outputEncoding('utf-16'))($expected[0])); } /** @@ -96,7 +96,7 @@ public function testCharsetConverterConvertsAnArray() ->inputEncoding('iso-8859-15') ->outputEncoding('utf-8') ; - $this->assertSame($expected, $converter->convert([$raw])[0]); + self::assertSame($expected, $converter->convert([$raw])[0]); } @@ -111,7 +111,7 @@ public function testCharsetConverterConvertsAnIterator() ->inputEncoding('iso-8859-15') ->outputEncoding('utf-8') ; - $this->assertInstanceOf(Iterator::class, $converter->convert($expected)); + self::assertInstanceOf(Iterator::class, $converter->convert($expected)); } /** @@ -129,8 +129,8 @@ public function testCharsetConverterAsStreamFilter() ->addStreamFilter('string.toupper'); CharsetConverter::addTo($csv, 'iso-8859-15', 'utf-8'); - $this->assertContains(CharsetConverter::FILTERNAME.'.*', stream_get_filters()); - $this->assertSame(strtoupper($expected), $csv->getContent()); + self::assertContains(CharsetConverter::FILTERNAME.'.*', stream_get_filters()); + self::assertSame(strtoupper($expected), $csv->getContent()); } /** @@ -139,7 +139,7 @@ public function testCharsetConverterAsStreamFilter() */ public function testCharsetConverterAsStreamFilterFailed() { - $this->expectException(Exception::class); + self::expectException(Exception::class); stream_filter_register(CharsetConverter::FILTERNAME.'.*', CharsetConverter::class); $expected = 'Batman,Superman,Anaïs'; $raw = mb_convert_encoding($expected, 'iso-8859-15', 'utf-8'); @@ -156,7 +156,7 @@ public function testOnCreateFailsWithWrongFiltername() { $converter = new CharsetConverter(); $converter->filtername = 'toto'; - $this->assertFalse($converter->onCreate()); + self::assertFalse($converter->onCreate()); } /** @@ -166,7 +166,7 @@ public function testOnCreateFailedWithWrongParams() { $converter = new CharsetConverter(); $converter->filtername = CharsetConverter::FILTERNAME.'.foo/bar'; - $this->assertFalse($converter->onCreate()); + self::assertFalse($converter->onCreate()); } /** @@ -181,7 +181,7 @@ public function testConvertOnlyStringField(array $record, array $expected) ->inputEncoding('iso-8859-15') ->outputEncoding('utf-8'); $res = $converter->convert([$record]); - $this->assertSame($expected, $res[0]); + self::assertSame($expected, $res[0]); } public function converterProvider() diff --git a/tests/ColumnConsistencyTest.php b/tests/ColumnConsistencyTest.php index 3511494e..9dd51e1d 100644 --- a/tests/ColumnConsistencyTest.php +++ b/tests/ColumnConsistencyTest.php @@ -55,13 +55,13 @@ public function testAutoDetect() $expected = ['jane', 'jane.doe@example.com']; $validator = new ColumnConsistency(); $this->csv->addValidator($validator, 'consistency'); - $this->assertSame(-1, $validator->getColumnCount()); + self::assertSame(-1, $validator->getColumnCount()); $this->csv->insertOne(['john', 'doe', 'john.doe@example.com']); - $this->assertSame(3, $validator->getColumnCount()); + self::assertSame(3, $validator->getColumnCount()); $this->csv->insertOne($expected); } catch (CannotInsertRecord $e) { - $this->assertSame($e->getName(), 'consistency'); - $this->assertEquals($e->getRecord(), ['jane', 'jane.doe@example.com']); + self::assertSame($e->getName(), 'consistency'); + self::assertEquals($e->getRecord(), ['jane', 'jane.doe@example.com']); } } @@ -72,7 +72,7 @@ public function testAutoDetect() */ public function testColumnsCount() { - $this->expectException(CannotInsertRecord::class); + self::expectException(CannotInsertRecord::class); $this->csv->addValidator(new ColumnConsistency(3), 'consistency'); $this->csv->insertOne(['john', 'doe', 'john.doe@example.com']); $this->csv->insertOne(['jane', 'jane.doe@example.com']); @@ -84,7 +84,7 @@ public function testColumnsCount() */ public function testColumsCountTriggersException() { - $this->expectException(Exception::class); + self::expectException(Exception::class); new ColumnConsistency(-2); } } diff --git a/tests/CsvTest.php b/tests/CsvTest.php index 73d2b24c..40e63754 100644 --- a/tests/CsvTest.php +++ b/tests/CsvTest.php @@ -26,6 +26,7 @@ use function chr; use function function_exists; use function iterator_to_array; +use function League\Csv\is_iterable as CSVIsiterable; use function strtolower; use function tmpfile; use function unlink; @@ -71,10 +72,10 @@ public function testCreateFromFileObjectPreserveFileObjectCsvControls() $file = new SplTempFileObject(); $file->setCsvControl($delimiter, $enclosure, $escape); $obj = Reader::createFromFileObject($file); - $this->assertSame($delimiter, $obj->getDelimiter()); - $this->assertSame($enclosure, $obj->getEnclosure()); + self::assertSame($delimiter, $obj->getDelimiter()); + self::assertSame($enclosure, $obj->getEnclosure()); if (3 === count($file->getCsvControl())) { - $this->assertSame($escape, $obj->getEscape()); + self::assertSame($escape, $obj->getEscape()); } } @@ -84,7 +85,7 @@ public function testCreateFromFileObjectPreserveFileObjectCsvControls() */ public function testCreateFromPathThrowsRuntimeException() { - $this->expectException(Exception::class); + self::expectException(Exception::class); Reader::createFromPath(__DIR__.'/foo/bar', 'r'); } @@ -98,7 +99,7 @@ public function testCreateFromPathThrowsRuntimeException() */ public function testGetInputBOM($expected, $str, $delimiter) { - $this->assertSame($expected, Reader::createFromString($str)->setDelimiter($delimiter)->getInputBOM()); + self::assertSame($expected, Reader::createFromString($str)->setDelimiter($delimiter)->getInputBOM()); } public function bomProvider() @@ -124,7 +125,7 @@ public function bomProvider() */ public function testCloningIsForbidden() { - $this->expectException(Exception::class); + self::expectException(Exception::class); clone $this->csv; } @@ -135,7 +136,7 @@ public function testCloningIsForbidden() */ public function testOutputSize() { - $this->assertSame(60, $this->csv->output('test.csv')); + self::assertSame(60, $this->csv->output('test.csv')); } /** @@ -144,7 +145,7 @@ public function testOutputSize() */ public function testInvalidOutputFile() { - $this->expectException(Exception::class); + self::expectException(Exception::class); $this->csv->output('invalid/file.csv'); } @@ -158,7 +159,7 @@ public function testInvalidOutputFile() public function testOutputHeaders() { if (!function_exists('xdebug_get_headers')) { - $this->markTestSkipped(); + self::markTestSkipped(sprintf('%s needs the xdebug extension to run', __METHOD__)); } $raw_csv = Reader::BOM_UTF8."john,doe,john.doe@example.com\njane,doe,jane.doe@example.com\n"; @@ -168,10 +169,10 @@ public function testOutputHeaders() // Due to the variety of ways the xdebug expresses Content-Type of text files, // we cannot count on complete string matching. - $this->assertContains('content-type: text/csv', strtolower($headers[0])); - $this->assertSame('Content-Transfer-Encoding: binary', $headers[1]); - $this->assertSame('Content-Description: File Transfer', $headers[2]); - $this->assertContains('Content-Disposition: attachment; filename="tst.csv"; filename*=utf-8\'\'t%C3%A9st.csv', $headers[3]); + self::assertContains('content-type: text/csv', strtolower($headers[0])); + self::assertSame('Content-Transfer-Encoding: binary', $headers[1]); + self::assertSame('Content-Description: File Transfer', $headers[2]); + self::assertContains('Content-Disposition: attachment; filename="tst.csv"; filename*=utf-8\'\'t%C3%A9st.csv', $headers[3]); } /** @@ -181,7 +182,7 @@ public function testOutputHeaders() public function testToString() { $expected = "john,doe,john.doe@example.com\njane,doe,jane.doe@example.com\n"; - $this->assertSame($expected, $this->csv->__toString()); + self::assertSame($expected, $this->csv->__toString()); } /** @@ -189,7 +190,7 @@ public function testToString() */ public function testChunkTriggersException() { - $this->expectException(Exception::class); + self::expectException(Exception::class); $chunk = $this->csv->chunk(0); iterator_to_array($chunk); } @@ -203,16 +204,16 @@ public function testChunk() $csv = Reader::createFromString($raw_csv)->setOutputBOM(Reader::BOM_UTF32_BE); $expected = Reader::BOM_UTF32_BE."john,doe,john.doe@example.com\njane,doe,jane.doe@example.com\n"; $res = ''; - foreach ($csv->chunk(8192) as $chunk) { + foreach ($csv->chunk(32) as $chunk) { $res .= $chunk; } - $this->assertSame($expected, $res); + self::assertSame($expected, $res); } public function testStreamFilterMode() { - $this->assertSame(STREAM_FILTER_READ, Reader::createFromString('')->getStreamFilterMode()); - $this->assertSame(STREAM_FILTER_WRITE, Writer::createFromString('')->getStreamFilterMode()); + self::assertSame(STREAM_FILTER_READ, Reader::createFromString('')->getStreamFilterMode()); + self::assertSame(STREAM_FILTER_WRITE, Writer::createFromString('')->getStreamFilterMode()); } /** @@ -221,10 +222,10 @@ public function testStreamFilterMode() */ public function testDelimeter() { - $this->expectException(Exception::class); + self::expectException(Exception::class); $this->csv->setDelimiter('o'); - $this->assertSame('o', $this->csv->getDelimiter()); - $this->assertSame($this->csv, $this->csv->setDelimiter('o')); + self::assertSame('o', $this->csv->getDelimiter()); + self::assertSame($this->csv, $this->csv->setDelimiter('o')); $this->csv->setDelimiter('foo'); } @@ -234,11 +235,11 @@ public function testDelimeter() */ public function testBOMSettings() { - $this->assertSame('', $this->csv->getOutputBOM()); + self::assertSame('', $this->csv->getOutputBOM()); $this->csv->setOutputBOM(Reader::BOM_UTF8); - $this->assertSame(Reader::BOM_UTF8, $this->csv->getOutputBOM()); + self::assertSame(Reader::BOM_UTF8, $this->csv->getOutputBOM()); $this->csv->setOutputBOM(''); - $this->assertSame('', $this->csv->getOutputBOM()); + self::assertSame('', $this->csv->getOutputBOM()); } /** @@ -250,7 +251,7 @@ public function testAddBOMSequences() $this->csv->setOutputBOM(Reader::BOM_UTF8); $expected = chr(239).chr(187).chr(191).'john,doe,john.doe@example.com'.PHP_EOL .'jane,doe,jane.doe@example.com'.PHP_EOL; - $this->assertSame($expected, $this->csv->getContent()); + self::assertSame($expected, $this->csv->getContent()); } /** @@ -263,7 +264,7 @@ public function testChangingBOMOnOutput() .'jane,doe,jane.doe@example.com'.PHP_EOL; $reader = Reader::createFromString(Reader::BOM_UTF32_BE.$text); $reader->setOutputBOM(Reader::BOM_UTF8); - $this->assertSame(Reader::BOM_UTF8.$text, $reader->getContent()); + self::assertSame(Reader::BOM_UTF8.$text, $reader->getContent()); } /** @@ -272,10 +273,10 @@ public function testChangingBOMOnOutput() */ public function testEscape() { - $this->expectException(Exception::class); + self::expectException(Exception::class); $this->csv->setEscape('o'); - $this->assertSame('o', $this->csv->getEscape()); - $this->assertSame($this->csv, $this->csv->setEscape('o')); + self::assertSame('o', $this->csv->getEscape()); + self::assertSame($this->csv, $this->csv->setEscape('o')); $this->csv->setEscape('foo'); } @@ -286,10 +287,10 @@ public function testEscape() */ public function testEnclosure() { - $this->expectException(Exception::class); + self::expectException(Exception::class); $this->csv->setEnclosure('o'); - $this->assertSame('o', $this->csv->getEnclosure()); - $this->assertSame($this->csv, $this->csv->setEnclosure('o')); + self::assertSame('o', $this->csv->getEnclosure()); + self::assertSame($this->csv, $this->csv->setEnclosure('o')); $this->csv->setEnclosure('foo'); } @@ -305,7 +306,7 @@ public function testAddStreamFilter() $csv->addStreamFilter('string.tolower'); $csv->addStreamFilter('string.toupper'); foreach ($csv as $row) { - $this->assertSame($row, ['WBUA', 'QBR', 'WBUA.QBR@RKNZCYR.PBZ']); + self::assertSame($row, ['WBUA', 'QBR', 'WBUA.QBR@RKNZCYR.PBZ']); } } @@ -316,9 +317,9 @@ public function testAddStreamFilter() */ public function testFailedAddStreamFilter() { - $this->expectException(Exception::class); + self::expectException(Exception::class); $csv = Writer::createFromFileObject(new SplTempFileObject()); - $this->assertFalse($csv->supportsStreamFilter()); + self::assertFalse($csv->supportsStreamFilter()); $csv->addStreamFilter('string.toupper'); } @@ -329,7 +330,7 @@ public function testFailedAddStreamFilter() */ public function testFailedAddStreamFilterWithWrongFilter() { - $this->expectException(Exception::class); + self::expectException(Exception::class); $csv = Writer::createFromStream(tmpfile()); $csv->addStreamFilter('foobar.toupper'); } @@ -343,9 +344,9 @@ public function testStreamFilterDetection() { $filtername = 'string.toupper'; $csv = Reader::createFromPath(__DIR__.'/data/foo.csv'); - $this->assertFalse($csv->hasStreamFilter($filtername)); + self::assertFalse($csv->hasStreamFilter($filtername)); $csv->addStreamFilter($filtername); - $this->assertTrue($csv->hasStreamFilter($filtername)); + self::assertTrue($csv->hasStreamFilter($filtername)); } /** @@ -356,9 +357,9 @@ public function testClearAttachedStreamFilters() $path = __DIR__.'/data/foo.csv'; $csv = Reader::createFromPath($path); $csv->addStreamFilter('string.toupper'); - $this->assertContains('JOHN', $csv->getContent()); + self::assertContains('JOHN', $csv->getContent()); $csv = Reader::createFromPath($path); - $this->assertNotContains('JOHN', $csv->getContent()); + self::assertNotContains('JOHN', $csv->getContent()); } /** @@ -370,7 +371,7 @@ public function testSetStreamFilterOnWriter() $csv = Writer::createFromPath(__DIR__.'/data/newline.csv', 'w+'); $csv->addStreamFilter('string.toupper'); $csv->insertOne([1, 'two', 3, "new\r\nline"]); - $this->assertContains("1,TWO,3,\"NEW\r\nLINE\"", $csv->getContent()); + self::assertContains("1,TWO,3,\"NEW\r\nLINE\"", $csv->getContent()); } /** @@ -384,7 +385,22 @@ public function testSetCsvControlWithDocument() ->setDelimiter(',') ->setEnclosure('"') ->setEscape('|'); - $this->assertSame('|', $csv->getEscape()); + self::assertSame('|', $csv->getEscape()); + } + + /** + * @covers \League\Csv\is_iterable + */ + public function testLeagueCsvIsIterable() + { + self::assertTrue(CSVIsiterable(['foo'])); + self::assertTrue(CSVIsiterable(Reader::createFromString(''))); + self::assertTrue(CSVIsiterable((function () { + yield 1; + })())); + self::assertFalse(CSVIsiterable(1)); + self::assertFalse(CSVIsiterable((object) ['foo'])); + self::assertFalse(CSVIsiterable(Writer::createFromString(''))); } /** @@ -393,16 +409,16 @@ public function testSetCsvControlWithDocument() public function testIsIterablePolyFill() { if (!version_compare(PHP_VERSION, '7.1.0', '<')) { - $this->markTestSkipped('Polyfill for PHP7.0'); + self::markTestSkipped(sprintf('%s test PHP7.0 Polyfill for is_iterable', __METHOD__)); } - $this->assertTrue(is_iterable(['foo'])); - $this->assertTrue(is_iterable(Reader::createFromString(''))); - $this->assertTrue(is_iterable((function () { + self::assertTrue(is_iterable(['foo'])); + self::assertTrue(is_iterable(Reader::createFromString(''))); + self::assertTrue(is_iterable((function () { yield 1; })())); - $this->assertFalse(is_iterable(1)); - $this->assertFalse(is_iterable((object) ['foo'])); - $this->assertFalse(is_iterable(Writer::createFromString(''))); + self::assertFalse(is_iterable(1)); + self::assertFalse(is_iterable((object) ['foo'])); + self::assertFalse(is_iterable(Writer::createFromString(''))); } } diff --git a/tests/DetectDelimiterTest.php b/tests/DetectDelimiterTest.php index 42126d50..9ebd0656 100644 --- a/tests/DetectDelimiterTest.php +++ b/tests/DetectDelimiterTest.php @@ -31,7 +31,7 @@ public function testDetectDelimiterListWithInvalidRowLimit() $file = new SplTempFileObject(); $file->fwrite("How are you today ?\nI'm doing fine thanks!"); $csv = Reader::createFromFileObject($file); - $this->expectException(Exception::class); + self::expectException(Exception::class); delimiter_detect($csv, [','], -4); } @@ -40,7 +40,7 @@ public function testDetectDelimiterListWithInvalidDelimiter() $file = new SplTempFileObject(); $file->fwrite("How are you today ?\nI'm doing fine thanks!"); $csv = Reader::createFromFileObject($file); - $this->expectException(TypeError::class); + self::expectException(TypeError::class); delimiter_detect($csv, [',', []]); } @@ -49,7 +49,7 @@ public function testDetectDelimiterListWithNoCSV() $file = new SplTempFileObject(); $file->fwrite("How are you today ?\nI'm doing fine thanks!"); $csv = Reader::createFromFileObject($file); - $this->assertSame(['toto' => 0, '|' => 0], delimiter_detect($csv, ['toto', '|'], 5)); + self::assertSame(['toto' => 0, '|' => 0], delimiter_detect($csv, ['toto', '|'], 5)); } public function testDetectDelimiterWithNoValidDelimiter() @@ -57,7 +57,7 @@ public function testDetectDelimiterWithNoValidDelimiter() $file = new SplTempFileObject(); $file->fwrite("How are you today ?\nI'm doing fine thanks!"); $csv = Reader::createFromFileObject($file); - $this->assertSame(['toto' => 0], delimiter_detect($csv, ['toto'], 5)); + self::assertSame(['toto' => 0], delimiter_detect($csv, ['toto'], 5)); } public function testDetectDelimiterListWithInconsistentCSV() @@ -71,7 +71,7 @@ public function testDetectDelimiterListWithInconsistentCSV() $data->fputcsv(['toto', 'tata', 'tutu']); $csv = Reader::createFromFileObject($data); - $this->assertSame(['|' => 12, ';' => 4], delimiter_detect($csv, ['|', ';'], 5)); + self::assertSame(['|' => 12, ';' => 4], delimiter_detect($csv, ['|', ';'], 5)); } public function testDetectDelimiterKeepOriginalDelimiter() @@ -81,6 +81,6 @@ public function testDetectDelimiterKeepOriginalDelimiter() $csv = Reader::createFromFileObject($file); $csv->setDelimiter('@'); $res = delimiter_detect($csv, ['toto', '|'], 5); - $this->assertSame('@', $csv->getDelimiter()); + self::assertSame('@', $csv->getDelimiter()); } } diff --git a/tests/EncloseFieldTest.php b/tests/EncloseFieldTest.php index 9917e757..88a8156a 100644 --- a/tests/EncloseFieldTest.php +++ b/tests/EncloseFieldTest.php @@ -53,9 +53,9 @@ public function testEncloseAll() $csv = Writer::createFromString(''); $csv->setDelimiter('|'); EncloseField::addTo($csv, "\t\x1f"); - $this->assertContains(EncloseField::getFiltername(), stream_get_filters()); + self::assertContains(EncloseField::getFiltername(), stream_get_filters()); $csv->insertAll($this->records); - $this->assertContains('"Grand Cherokee"', $csv->getContent()); + self::assertContains('"Grand Cherokee"', $csv->getContent()); } /** @@ -67,7 +67,7 @@ public function testOnCreateFailedWithWrongParams(array $params) { $filter = new EncloseField(); $filter->params = $params; - $this->assertFalse($filter->onCreate()); + self::assertFalse($filter->onCreate()); } public function wrongParamProvider() @@ -89,7 +89,7 @@ public function wrongParamProvider() */ public function testEncloseFieldImmutability() { - $this->expectException(InvalidArgumentException::class); + self::expectException(InvalidArgumentException::class); $csv = Writer::createFromString(''); $csv->setDelimiter('|'); EncloseField::addTo($csv, 'foo'); diff --git a/tests/EscapeFormulaTest.php b/tests/EscapeFormulaTest.php index 7109cb3c..c926c9fc 100644 --- a/tests/EscapeFormulaTest.php +++ b/tests/EscapeFormulaTest.php @@ -33,7 +33,7 @@ class EscapeFormulaTest extends TestCase */ public function testConstructorThrowsTypError() { - $this->expectException(TypeError::class); + self::expectException(TypeError::class); new EscapeFormula("\t", [(object) 'i']); } @@ -44,7 +44,7 @@ public function testConstructorThrowsTypError() */ public function testConstructorThrowsInvalidArgumentException() { - $this->expectException(InvalidArgumentException::class); + self::expectException(InvalidArgumentException::class); new EscapeFormula("\t", ['i', 'foo']); } @@ -55,9 +55,9 @@ public function testConstructorThrowsInvalidArgumentException() public function testGetEscape() { $formatter = new EscapeFormula(); - $this->assertSame("\t", $formatter->getEscape()); + self::assertSame("\t", $formatter->getEscape()); $formatterBis = new EscapeFormula("\n"); - $this->assertSame("\n", $formatterBis->getEscape()); + self::assertSame("\n", $formatterBis->getEscape()); } /** @@ -68,9 +68,9 @@ public function testGetEscape() public function testGetSpecialChars() { $formatter = new EscapeFormula(); - $this->assertNotContains('i', $formatter->getSpecialCharacters()); + self::assertNotContains('i', $formatter->getSpecialCharacters()); $formatterBis = new EscapeFormula("\t", ['i']); - $this->assertContains('i', $formatterBis->getSpecialCharacters()); + self::assertContains('i', $formatterBis->getSpecialCharacters()); } /** @@ -83,7 +83,7 @@ public function testEscapeRecord() $record = ['2', '2017-07-25', 'Important Client', '=2+5', 240, null, (object) 'yes']; $expected = ['2', '2017-07-25', 'Important Client', "\t=2+5", 240, null, (object) 'yes']; $formatter = new EscapeFormula(); - $this->assertEquals($expected, $formatter->escapeRecord($record)); + self::assertEquals($expected, $formatter->escapeRecord($record)); } /** @@ -99,6 +99,6 @@ public function testFormatterOnWriter() $csv = Writer::createFromFileObject(new SplTempFileObject()); $csv->addFormatter(new EscapeFormula()); $csv->insertOne($record); - $this->assertContains($expected, $csv->getContent()); + self::assertContains($expected, $csv->getContent()); } } diff --git a/tests/HTMLConverterTest.php b/tests/HTMLConverterTest.php index f7053f9e..3a172f6a 100644 --- a/tests/HTMLConverterTest.php +++ b/tests/HTMLConverterTest.php @@ -54,9 +54,9 @@ public function testToHTML() ; $html = $converter->convert($records); - $this->assertContains('', $html); - $this->assertContains('', $html); + self::assertContains('expectException(DOMException::class); + self::expectException(DOMException::class); (new HTMLConverter())->table('table-csv-data', 'te st'); } } diff --git a/tests/RFC4180FieldTest.php b/tests/RFC4180FieldTest.php index 5465691b..75d83fb3 100644 --- a/tests/RFC4180FieldTest.php +++ b/tests/RFC4180FieldTest.php @@ -49,10 +49,10 @@ public function testStreamFilterOnWrite($expected, array $record) { $csv = Writer::createFromPath('php://temp'); RFC4180Field::addTo($csv); - $this->assertContains(RFC4180Field::getFiltername(), stream_get_filters()); + self::assertContains(RFC4180Field::getFiltername(), stream_get_filters()); $csv->setNewline("\r\n"); $csv->insertOne($record); - $this->assertSame($expected, $csv->getContent()); + self::assertSame($expected, $csv->getContent()); } public function bugsProvider() @@ -87,7 +87,7 @@ public function testStreamFilterOnRead($expected, array $record) { $csv = Reader::createFromString($expected); RFC4180Field::addTo($csv); - $this->assertSame($record, $csv->fetchOne(0)); + self::assertSame($record, $csv->fetchOne(0)); } public function readerBugsProvider() @@ -106,7 +106,7 @@ public function readerBugsProvider() */ public function testOnCreateFailedWithoutParams() { - $this->expectException(TypeError::class); + self::expectException(TypeError::class); (new RFC4180Field())->onCreate(); } @@ -119,7 +119,7 @@ public function testOnCreateFailedWithWrongParams(array $params) { $filter = new RFC4180Field(); $filter->params = $params; - $this->assertFalse($filter->onCreate()); + self::assertFalse($filter->onCreate()); } public function wrongParamProvider() @@ -177,8 +177,8 @@ public function testDoNotEncloseWhiteSpacedField() RFC4180Field::addTo($csv, "\0"); $csv->insertAll($this->records); $contents = $csv->getContent(); - $this->assertContains('Grand Cherokee', $contents); - $this->assertNotContains('"Grand Cherokee"', $contents); + self::assertContains('Grand Cherokee', $contents); + self::assertNotContains('"Grand Cherokee"', $contents); } @@ -190,7 +190,7 @@ public function testDoNotEncloseWhiteSpacedField() */ public function testDoNotEncloseWhiteSpacedFieldThrowsException() { - $this->expectException(InvalidArgumentException::class); + self::expectException(InvalidArgumentException::class); RFC4180Field::addTo(Writer::createFromString(''), "\t\0"); } } diff --git a/tests/ReaderTest.php b/tests/ReaderTest.php index f833c918..f4c78b5d 100644 --- a/tests/ReaderTest.php +++ b/tests/ReaderTest.php @@ -70,10 +70,10 @@ public function testCountable() $source = '"parent name","child name","title" "parentA","childA","titleA"'; $csv = Reader::createFromString($source); - $this->assertCount(2, $csv); - $this->assertCount(2, $csv); + self::assertCount(2, $csv); + self::assertCount(2, $csv); $csv->setHeaderOffset(0); - $this->assertCount(1, $csv); + self::assertCount(1, $csv); } /** @@ -87,17 +87,17 @@ public function testGetIterator() { $this->csv->setHeaderOffset(0); foreach ($this->csv as $record) { - $this->assertCount(4, $record); + self::assertCount(4, $record); } $this->csv->setHeaderOffset(1); foreach ($this->csv as $record) { - $this->assertCount(3, $record); + self::assertCount(3, $record); } $this->csv->setHeaderOffset(null); foreach ($this->csv->getRecords() as $record) { - $this->assertTrue(in_array(count($record), [3, 4], true)); + self::assertTrue(in_array(count($record), [3, 4], true)); } } @@ -109,7 +109,7 @@ public function testCombineHeader() { $this->csv->setHeaderOffset(1); foreach ($this->csv as $record) { - $this->assertSame(['jane', 'doe', 'jane.doe@example.com'], array_keys($record)); + self::assertSame(['jane', 'doe', 'jane.doe@example.com'], array_keys($record)); } } @@ -122,11 +122,11 @@ public function testCombineHeader() public function testGetHeader() { $this->csv->setHeaderOffset(1); - $this->assertSame(1, $this->csv->getHeaderOffset()); - $this->assertSame($this->expected[1], $this->csv->getHeader()); + self::assertSame(1, $this->csv->getHeaderOffset()); + self::assertSame($this->expected[1], $this->csv->getHeader()); $this->csv->setHeaderOffset(null); - $this->assertNull($this->csv->getHeaderOffset()); - $this->assertSame([], $this->csv->getHeader()); + self::assertNull($this->csv->getHeaderOffset()); + self::assertSame([], $this->csv->getHeader()); } /** @@ -150,9 +150,9 @@ public function testCall() $csv->setHeaderOffset(0); $res = (new Statement())->process($csv); - $this->assertEquals($csv->fetchOne(3), $res->fetchOne(3)); - $this->assertEquals($csv->fetchColumn('firstname'), $res->fetchColumn('firstname')); - $this->assertEquals($csv->fetchPairs('lastname', 0), $res->fetchPairs('lastname', 0)); + self::assertEquals($csv->fetchOne(3), $res->fetchOne(3)); + self::assertEquals($csv->fetchColumn('firstname'), $res->fetchColumn('firstname')); + self::assertEquals($csv->fetchPairs('lastname', 0), $res->fetchPairs('lastname', 0)); } /** @@ -163,7 +163,7 @@ public function testCall() */ public function testCallThrowsException($method) { - $this->expectException(BadMethodCallException::class); + self::expectException(BadMethodCallException::class); $this->csv->$method(); } @@ -184,14 +184,14 @@ public function invalidMethodCallMethodProvider() */ public function testHeaderThrowsExceptionOnError() { - $this->expectException(Exception::class); + self::expectException(Exception::class); $csv = Reader::createFromString( 'field1,field1,field3 1,2,3 4,5,6' ); $csv->setHeaderOffset(0); - $this->assertSame(['field1', 'field1', 'field3'], $csv->getHeader()); + self::assertSame(['field1', 'field1', 'field3'], $csv->getHeader()); iterator_to_array($csv); } @@ -207,9 +207,9 @@ public function testStripBOM(array $record, string $expected_bom, string $expect $fp = fopen('php://temp', 'r+'); fputcsv($fp, $record); $csv = Reader::createFromStream($fp); - $this->assertSame($expected_bom, $csv->getInputBOM()); + self::assertSame($expected_bom, $csv->getInputBOM()); foreach ($csv as $offset => $record) { - $this->assertSame($expected, $record[0]); + self::assertSame($expected, $record[0]); } $csv = null; fclose($fp); @@ -251,7 +251,7 @@ public function testStripBOMWithEnclosure() $csv->setHeaderOffset(0); $expected = ['parent name' => 'parentA', 'child name' => 'childA', 'title' => 'titleA']; foreach ($csv->getRecords() as $offset => $record) { - $this->assertSame($expected, $record); + self::assertSame($expected, $record); } } @@ -269,7 +269,7 @@ public function testStripNoBOM() $csv->setHeaderOffset(0); $expected = ['parent name' => 'parentA', 'child name' => 'childA', 'title' => 'titleA']; foreach ($csv->getRecords() as $offset => $record) { - $this->assertSame($expected, $record); + self::assertSame($expected, $record); } } @@ -284,7 +284,7 @@ public function testAppliedFlags(int $flag, int $fetch_count) $obj->fwrite("1st\n2nd\n"); $obj->setFlags($flag); $reader = Reader::createFromFileObject($obj); - $this->assertCount($fetch_count, $reader); + self::assertCount($fetch_count, $reader); $reader = null; $obj = null; unlink($path); @@ -310,7 +310,7 @@ public function appliedFlagsProvider() */ public function testGetHeaderThrowsExceptionWithNegativeOffset() { - $this->expectException(Exception::class); + self::expectException(Exception::class); $this->csv->setHeaderOffset(-3)->getRecords(); } @@ -320,7 +320,7 @@ public function testGetHeaderThrowsExceptionWithNegativeOffset() */ public function testGetHeaderThrowsExceptionWithSplFileObject() { - $this->expectException(Exception::class); + self::expectException(Exception::class); $this->csv->setHeaderOffset(23)->getRecords(); } @@ -330,7 +330,7 @@ public function testGetHeaderThrowsExceptionWithSplFileObject() */ public function testGetHeaderThrowsExceptionWithStreamObject() { - $this->expectException(Exception::class); + self::expectException(Exception::class); $tmp = fopen('php://temp', 'r+'); foreach ($this->expected as $row) { @@ -347,7 +347,7 @@ public function testGetHeaderThrowsExceptionWithStreamObject() */ public function testSetHeaderThrowsExceptionOnWrongInput() { - $this->expectException(TypeError::class); + self::expectException(TypeError::class); $this->csv->setHeaderOffset((object) 1); } @@ -356,7 +356,7 @@ public function testSetHeaderThrowsExceptionOnWrongInput() */ public function testSetHeaderThrowsExceptionOnWrongInputRange() { - $this->expectException(Exception::class); + self::expectException(Exception::class); $this->csv->setHeaderOffset(-1); } @@ -368,7 +368,7 @@ public function testMapRecordsFields() $keys = ['firstname', 'lastname', 'email']; $res = $this->csv->getRecords($keys); foreach ($res as $record) { - $this->assertSame($keys, array_keys($record)); + self::assertSame($keys, array_keys($record)); } } @@ -388,7 +388,7 @@ public function testJsonSerialize() } $reader = Reader::createFromFileObject($tmp)->setHeaderOffset(0); - $this->assertSame( + self::assertSame( '[{"First Name":"jane","Last Name":"doe","E-mail":"jane.doe@example.com"}]', json_encode($reader) ); @@ -400,6 +400,6 @@ public function testJsonSerialize() public function testCreateFromPath() { $csv = Reader::createFromPath(__DIR__.'/data/foo_readonly.csv'); - $this->assertCount(1, $csv); + self::assertCount(1, $csv); } } diff --git a/tests/ResultSetTest.php b/tests/ResultSetTest.php index 5d4e8346..0653f357 100644 --- a/tests/ResultSetTest.php +++ b/tests/ResultSetTest.php @@ -69,7 +69,7 @@ public function tearDown() */ public function testSetLimit() { - $this->assertCount(1, $this->stmt->limit(1)->process($this->csv)); + self::assertCount(1, $this->stmt->limit(1)->process($this->csv)); } /** @@ -77,7 +77,7 @@ public function testSetLimit() */ public function testSetOffsetThrowsException() { - $this->expectException(Exception::class); + self::expectException(Exception::class); $this->stmt->offset(-1); } @@ -91,8 +91,8 @@ public function testSetOffsetThrowsException() public function testCountable() { $records = $this->stmt->limit(1)->process($this->csv); - $this->assertCount(1, $records); - $this->assertInstanceOf(Generator::class, $records->getIterator()); + self::assertCount(1, $records); + self::assertInstanceOf(Generator::class, $records->getIterator()); } /** @@ -103,7 +103,7 @@ public function testStatementSameInstance() { $stmt_alt = $this->stmt->limit(-1)->offset(0); - $this->assertSame($stmt_alt, $this->stmt); + self::assertSame($stmt_alt, $this->stmt); } /** @@ -111,7 +111,7 @@ public function testStatementSameInstance() */ public function testSetLimitThrowException() { - $this->expectException(Exception::class); + self::expectException(Exception::class); $this->stmt->limit(-4); } @@ -121,7 +121,7 @@ public function testSetLimitThrowException() */ public function testSetOffset() { - $this->assertContains( + self::assertContains( ['jane', 'doe', 'jane.doe@example.com'], $this->stmt->offset(1)->process($this->csv) ); @@ -135,7 +135,7 @@ public function testSetOffset() */ public function testInterval(int $offset, int $limit) { - $this->assertContains( + self::assertContains( ['jane', 'doe', 'jane.doe@example.com'], $this->stmt ->offset($offset) @@ -165,7 +165,7 @@ public function intervalTest() */ public function testIntervalThrowException() { - $this->expectException(OutOfBoundsException::class); + self::expectException(OutOfBoundsException::class); iterator_to_array($this->stmt ->offset(1) ->limit(0) @@ -181,7 +181,7 @@ public function testFilter() return !in_array('jane', $row, true); }; - $this->assertNotContains( + self::assertNotContains( ['jane', 'doe', 'jane.doe@example.com'], iterator_to_array($this->stmt->where($func)->process($this->csv), false) ); @@ -196,7 +196,7 @@ public function testOrderBy() $func = function (array $rowA, array $rowB): int { return strcmp($rowA[0], $rowB[0]); }; - $this->assertSame( + self::assertSame( array_reverse($this->expected), iterator_to_array($this->stmt->orderBy($func)->process($this->csv), false) ); @@ -212,7 +212,7 @@ public function testOrderByWithEquity() return strlen($rowA[0]) <=> strlen($rowB[0]); }; - $this->assertSame( + self::assertSame( $this->expected, iterator_to_array($this->stmt->orderBy($func)->process($this->csv), false) ); @@ -230,7 +230,7 @@ public function testOrderByWithEquity() */ public function testFetchColumnTriggersException($field) { - $this->expectException(Exception::class); + self::expectException(Exception::class); $this->csv->setHeaderOffset(0); $res = $this->stmt->process($this->csv)->fetchColumn($field); iterator_to_array($res, false); @@ -251,7 +251,7 @@ public function invalidFieldNameProvider() */ public function testFetchColumnTriggersOutOfRangeException() { - $this->expectException(Exception::class); + self::expectException(Exception::class); $this->csv->setHeaderOffset(0); $res = $this->stmt->process($this->csv)->fetchColumn(-1); iterator_to_array($res, false); @@ -277,7 +277,7 @@ public function testFetchAssocWithRowIndex() $csv = Reader::createFromFileObject($tmp); $csv->setHeaderOffset(2); - $this->assertContains( + self::assertContains( ['D' => '6', 'E' => '7', 'F' => '8'], iterator_to_array($this->stmt->process($csv), false) ); @@ -296,8 +296,8 @@ public function testFetchColumnWithColumnname() "parentA","childA","titleA"'; $csv = Reader::createFromString($source); $csv->setHeaderOffset(0); - $this->assertContains('parentA', $this->stmt->process($csv)->fetchColumn('parent name')); - $this->assertContains('parentA', $this->stmt->process($csv)->fetchColumn(0)); + self::assertContains('parentA', $this->stmt->process($csv)->fetchColumn('parent name')); + self::assertContains('parentA', $this->stmt->process($csv)->fetchColumn(0)); } /** @@ -309,8 +309,8 @@ public function testFetchColumnWithColumnname() */ public function testFetchColumn() { - $this->assertContains('john', $this->stmt->process($this->csv)->fetchColumn(0)); - $this->assertContains('jane', $this->stmt->process($this->csv)->fetchColumn()); + self::assertContains('john', $this->stmt->process($this->csv)->fetchColumn(0)); + self::assertContains('jane', $this->stmt->process($this->csv)->fetchColumn()); } /** @@ -332,7 +332,7 @@ public function testFetchColumnInconsistentColumnCSV() } $csv = Reader::createFromFileObject($file); $res = $this->stmt->process($csv)->fetchColumn(2); - $this->assertCount(1, iterator_to_array($res)); + self::assertCount(1, iterator_to_array($res)); } /** @@ -354,7 +354,7 @@ public function testFetchColumnEmptyCol() } $csv = Reader::createFromFileObject($file); $res = $this->stmt->process($csv)->fetchColumn(2); - $this->assertCount(0, iterator_to_array($res)); + self::assertCount(0, iterator_to_array($res)); } /** @@ -362,9 +362,9 @@ public function testFetchColumnEmptyCol() */ public function testfetchOne() { - $this->assertSame($this->expected[0], $this->stmt->process($this->csv)->fetchOne(0)); - $this->assertSame($this->expected[1], $this->stmt->process($this->csv)->fetchOne(1)); - $this->assertSame([], $this->stmt->process($this->csv)->fetchOne(35)); + self::assertSame($this->expected[0], $this->stmt->process($this->csv)->fetchOne(0)); + self::assertSame($this->expected[1], $this->stmt->process($this->csv)->fetchOne(1)); + self::assertSame([], $this->stmt->process($this->csv)->fetchOne(35)); } /** @@ -372,7 +372,7 @@ public function testfetchOne() */ public function testFetchOneTriggersException() { - $this->expectException(Exception::class); + self::expectException(Exception::class); $this->stmt->process($this->csv)->fetchOne(-5); } @@ -388,7 +388,7 @@ public function testFetchPairsIteratorMode($key, $value, array $expected) $iterator = $this->stmt->process($this->csv)->fetchPairs($key, $value); foreach ($iterator as $key => $value) { $res = current($expected); - $this->assertSame($value, $res[$key]); + self::assertSame($value, $res[$key]); next($expected); } } @@ -421,7 +421,7 @@ public function fetchPairsDataProvider() */ public function testFetchPairsWithInvalidOffset() { - $this->assertCount(0, iterator_to_array($this->stmt->process($this->csv)->fetchPairs(10, 1), true)); + self::assertCount(0, iterator_to_array($this->stmt->process($this->csv)->fetchPairs(10, 1), true)); } /** @@ -432,7 +432,7 @@ public function testFetchPairsWithInvalidValue() { $res = $this->stmt->process($this->csv)->fetchPairs(0, 15); foreach ($res as $value) { - $this->assertNull($value); + self::assertNull($value); } } @@ -442,11 +442,11 @@ public function testFetchPairsWithInvalidValue() public function testGetHeader() { $expected = ['firstname', 'lastname', 'email']; - $this->assertSame([], $this->stmt->process($this->csv)->getHeader()); - $this->assertSame($expected, $this->stmt->process($this->csv, $expected)->getHeader()); + self::assertSame([], $this->stmt->process($this->csv)->getHeader()); + self::assertSame($expected, $this->stmt->process($this->csv, $expected)->getHeader()); $this->csv->setHeaderOffset(0); - $this->assertSame($this->expected[0], $this->stmt->process($this->csv)->getHeader()); - $this->assertSame($expected, $this->stmt->process($this->csv, $expected)->getHeader()); + self::assertSame($this->expected[0], $this->stmt->process($this->csv)->getHeader()); + self::assertSame($expected, $this->stmt->process($this->csv, $expected)->getHeader()); } /** @@ -456,7 +456,7 @@ public function testGetHeader() public function testGetRecords() { $result = $this->stmt->process($this->csv); - $this->assertEquals($result->getIterator(), $result->getRecords()); + self::assertEquals($result->getIterator(), $result->getRecords()); } /** @@ -477,7 +477,7 @@ public function testJsonSerialize() $reader = Reader::createFromFileObject($tmp)->setHeaderOffset(0); $result = (new Statement())->offset(1)->limit(1)->process($reader); - $this->assertSame( + self::assertSame( '[{"First Name":"jane","Last Name":"doe","E-mail":"jane.doe@example.com"}]', json_encode($result) ); diff --git a/tests/StreamTest.php b/tests/StreamTest.php index db18f7d6..4b29e18b 100644 --- a/tests/StreamTest.php +++ b/tests/StreamTest.php @@ -48,7 +48,7 @@ public function tearDown() */ public function testCloningIsForbidden() { - $this->expectException(Exception::class); + self::expectException(Exception::class); $toto = clone new Stream(fopen('php://temp', 'r+')); } @@ -57,7 +57,7 @@ public function testCloningIsForbidden() */ public function testCreateStreamWithInvalidParameter() { - $this->expectException(TypeError::class); + self::expectException(TypeError::class); new Stream(__DIR__.'/data/foo.csv'); } @@ -66,7 +66,7 @@ public function testCreateStreamWithInvalidParameter() */ public function testCreateStreamWithWrongResourceType() { - $this->expectException(TypeError::class); + self::expectException(TypeError::class); new Stream(curl_init()); } @@ -76,8 +76,8 @@ public function testCreateStreamWithWrongResourceType() public function testCreateStreamFromPath() { $path = 'no/such/file.csv'; - $this->expectException(Exception::class); - $this->expectExceptionMessage('`'.$path.'`: failed to open stream: No such file or directory'); + self::expectException(Exception::class); + self::expectExceptionMessage('`'.$path.'`: failed to open stream: No such file or directory'); Stream::createFromPath($path); } @@ -104,7 +104,7 @@ public function testCreateStreamFromPathWithContext() ); $stream->setFlags(SplFileObject::READ_AHEAD); $stream->rewind(); - $this->assertInternalType('array', $stream->current()); + self::assertInternalType('array', $stream->current()); } /** @@ -119,7 +119,7 @@ public function testCreateStreamFromPathWithContext() */ public function testfputcsv($delimiter, $enclosure, $escape) { - $this->expectException(Exception::class); + self::expectException(Exception::class); $stream = new Stream(fopen('php://temp', 'r+')); $stream->fputcsv(['john', 'doe', 'john.doe@example.com'], $delimiter, $enclosure, $escape); } @@ -139,7 +139,7 @@ public function fputcsvProvider() public function testVarDump() { $stream = new Stream(fopen('php://temp', 'r+')); - $this->assertInternalType('array', $stream->__debugInfo()); + self::assertInternalType('array', $stream->__debugInfo()); } /** @@ -147,7 +147,7 @@ public function testVarDump() */ public function testSeekThrowsException() { - $this->expectException(Exception::class); + self::expectException(Exception::class); $stream = new Stream(fopen('php://temp', 'r+')); $stream->seek(-1); } @@ -161,7 +161,7 @@ public function testSeek() $doc->setCsvControl(';'); $doc->setFlags(SplFileObject::READ_CSV); $doc->seek(1); - $this->assertSame(['Aaron', '55', 'M', '2004'], $doc->current()); + self::assertSame(['Aaron', '55', 'M', '2004'], $doc->current()); } /** @@ -169,7 +169,7 @@ public function testSeek() */ public function testRewindThrowsException() { - $this->expectException(Exception::class); + self::expectException(Exception::class); $stream = new Stream(fopen('php://stdin', 'r')); $stream->rewind(); } @@ -179,7 +179,7 @@ public function testRewindThrowsException() */ public function testCreateStreamWithNonSeekableStream() { - $this->expectException(Exception::class); + self::expectException(Exception::class); $stream = new Stream(fopen('php://stdin', 'r')); $stream->seek(3); } @@ -192,11 +192,11 @@ public function testCreateStreamWithNonSeekableStream() public function testCsvControl() { $doc = Stream::createFromString('foo,bar'); - $this->assertSame([',', '"', '\\'], $doc->getCsvControl()); + self::assertSame([',', '"', '\\'], $doc->getCsvControl()); $expected = [';', '|', '"']; $doc->setCsvControl(...$expected); - $this->assertSame($expected, $doc->getCsvControl()); - $this->expectException(Exception::class); + self::assertSame($expected, $doc->getCsvControl()); + self::expectException(Exception::class); $doc->setCsvControl(...['foo']); } @@ -206,8 +206,8 @@ public function testCsvControl() public function testAppendStreamFilterThrowsException() { $filtername = 'foo.bar'; - $this->expectException(Exception::class); - $this->expectExceptionMessage('unable to locate filter `'.$filtername.'`'); + self::expectException(Exception::class); + self::expectExceptionMessage('unable to locate filter `'.$filtername.'`'); $stream = Stream::createFromPath('php://temp', 'r+'); $stream->appendFilter($filtername, STREAM_FILTER_READ); } diff --git a/tests/WriterTest.php b/tests/WriterTest.php index 77393158..5f3259df 100644 --- a/tests/WriterTest.php +++ b/tests/WriterTest.php @@ -57,7 +57,8 @@ public function tearDown() public function testflushThreshold() { $this->csv->setFlushThreshold(12); - $this->assertSame(12, $this->csv->getFlushThreshold()); + self::assertSame(12, $this->csv->getFlushThreshold()); + self::assertSame($this->csv, $this->csv->setFlushThreshold(12)); } /** @@ -66,7 +67,7 @@ public function testflushThreshold() public function testflushThresholdThrowsException() { $this->csv->setFlushThreshold(1); - $this->expectException(Exception::class); + self::expectException(Exception::class); $this->csv->setFlushThreshold(0); } @@ -76,14 +77,14 @@ public function testflushThresholdThrowsException() */ public function testflushThresholdThrowsTypeError() { - $this->expectException(TypeError::class); + self::expectException(TypeError::class); $this->csv->setFlushThreshold((object) 12); } public function testSupportsStreamFilter() { $csv = Writer::createFromPath(__DIR__.'/data/foo.csv'); - $this->assertTrue($csv->supportsStreamFilter()); + self::assertTrue($csv->supportsStreamFilter()); $csv->setFlushThreshold(3); $csv->addStreamFilter('string.toupper'); $csv->insertOne(['jane', 'doe', 'jane@example.com']); @@ -95,7 +96,7 @@ public function testSupportsStreamFilter() $csv->insertOne(['jane', 'doe', 'jane@example.com']); $csv->insertOne(['jane', 'doe', 'jane@example.com']); $csv->setFlushThreshold(null); - $this->assertContains('JANE,DOE,JANE@EXAMPLE.COM', $csv->getContent()); + self::assertContains('JANE,DOE,JANE@EXAMPLE.COM', $csv->getContent()); } /** @@ -109,7 +110,7 @@ public function testInsert() foreach ($expected as $row) { $this->csv->insertOne($row); } - $this->assertContains('john,doe,john.doe@example.com', $this->csv->getContent()); + self::assertContains('john,doe,john.doe@example.com', $this->csv->getContent()); } /** @@ -119,16 +120,26 @@ public function testInsertNormalFile() { $csv = Writer::createFromPath(__DIR__.'/data/foo.csv', 'a+'); $csv->insertOne(['jane', 'doe', 'jane.doe@example.com']); - $this->assertContains('jane,doe,jane.doe@example.com', $csv->getContent()); + self::assertContains('jane,doe,jane.doe@example.com', $csv->getContent()); } /** * @covers ::insertOne + * @dataProvider inputDataProvider */ - public function testInsertThrowsExceptionOnError() + public function testInsertThrowsExceptionOnError(array $record) { - $csv = Writer::createFromPath(__DIR__.'/data/foo.csv', 'r'); - $this->assertSame(0, $csv->insertOne(['jane', 'doe', 'jane.doe@example.com'])); + self::expectException(CannotInsertRecord::class); + self::expectExceptionMessage('Unable to write record to the CSV document'); + Writer::createFromPath(__DIR__.'/data/foo.csv', 'r')->insertOne($record); + } + + public function inputDataProvider() + { + return [ + 'normal record' => [['foo', 'bar']], + 'empty record' => [[]], + ]; } /** @@ -136,7 +147,7 @@ public function testInsertThrowsExceptionOnError() */ public function testFailedSaveWithWrongType() { - $this->expectException(TypeError::class); + self::expectException(TypeError::class); $this->csv->insertAll(new stdClass()); } @@ -149,7 +160,7 @@ public function testFailedSaveWithWrongType() public function testSave($argument, string $expected) { $this->csv->insertAll($argument); - $this->assertContains($expected, $this->csv->getContent()); + self::assertContains($expected, $this->csv->getContent()); } public function dataToSave() @@ -180,7 +191,7 @@ public function testToString() } $expected = "john|doe|john.doe@example.com\njane|doe|jane.doe@example.com\n"; - $this->assertSame($expected, $csv->getContent()); + self::assertSame($expected, $csv->getContent()); $csv = null; fclose($fp); $fp = null; @@ -196,10 +207,10 @@ public function testToString() public function testCustomNewline() { $csv = Writer::createFromStream(tmpfile()); - $this->assertSame("\n", $csv->getNewline()); + self::assertSame("\n", $csv->getNewline()); $csv->setNewline("\r\n"); $csv->insertOne(['jane', 'doe']); - $this->assertSame("jane,doe\r\n", $csv->getContent()); + self::assertSame("jane,doe\r\n", $csv->getContent()); $csv = null; } @@ -209,7 +220,7 @@ public function testAddValidationRules() return false; }; - $this->expectException(CannotInsertRecord::class); + self::expectException(CannotInsertRecord::class); $this->csv->addValidator($func, 'func1'); $this->csv->insertOne(['jane', 'doe']); } @@ -222,7 +233,7 @@ public function testFormatterRules() $this->csv->addFormatter($func); $this->csv->insertOne(['jane', 'doe']); - $this->assertSame("JANE,DOE\n", $this->csv->getContent()); + self::assertSame("JANE,DOE\n", $this->csv->getContent()); } /** @@ -230,7 +241,7 @@ public function testFormatterRules() */ public function testWriterTriggerExceptionWithNonSeekableStream() { - $this->expectException(Exception::class); + self::expectException(Exception::class); $writer = Writer::createFromPath('php://output', 'w'); $writer->setNewline("\r\n"); $writer->insertOne(['foo', 'bar']); diff --git a/tests/XMLConverterTest.php b/tests/XMLConverterTest.php index 30dbb8bd..4593b694 100644 --- a/tests/XMLConverterTest.php +++ b/tests/XMLConverterTest.php @@ -64,12 +64,12 @@ public function testToXML() $record_list = $dom->getElementsByTagName('record'); $field_list = $dom->getElementsByTagName('field'); - $this->assertInstanceOf(DOMDocument::class, $dom); - $this->assertSame('csv', $dom->documentElement->tagName); - $this->assertEquals(5, $record_list->length); - $this->assertTrue($record_list->item(0)->hasAttribute('offset')); - $this->assertEquals(20, $field_list->length); - $this->assertTrue($field_list->item(0)->hasAttribute('name')); + self::assertInstanceOf(DOMDocument::class, $dom); + self::assertSame('csv', $dom->documentElement->tagName); + self::assertEquals(5, $record_list->length); + self::assertTrue($record_list->item(0)->hasAttribute('offset')); + self::assertEquals(20, $field_list->length); + self::assertTrue($field_list->item(0)->hasAttribute('name')); } /** @@ -79,7 +79,7 @@ public function testToXML() */ public function testXmlElementTriggersException() { - $this->expectException(DOMException::class); + self::expectException(DOMException::class); (new XMLConverter()) ->recordElement('record', '') ->rootElement(' '); @@ -90,7 +90,7 @@ public function testXmlElementTriggersException() */ public function testXmlElementTriggersTypeError() { - $this->expectException(TypeError::class); + self::expectException(TypeError::class); (new XMLConverter())->convert('foo'); } } From bbf0dfd233f8743f1da1abab9b41b9b77efb7be7 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Tue, 14 Aug 2018 21:01:58 +0200 Subject: [PATCH 034/858] missing function import --- CHANGELOG.md | 2 +- src/Stream.php | 2 +- src/XMLConverter.php | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab19bc20..07d8e4bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All Notable changes to `Csv` will be documented in this file -## Next - TBD +## 9.1.5 - TBD ### Added diff --git a/src/Stream.php b/src/Stream.php index f708cc27..32344f08 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -19,7 +19,7 @@ use SeekableIterator; use SplFileObject; use TypeError; - +use const SEEK_SET; use function array_keys; use function array_values; use function array_walk_recursive; diff --git a/src/XMLConverter.php b/src/XMLConverter.php index 5e89e5ce..c6662c1f 100644 --- a/src/XMLConverter.php +++ b/src/XMLConverter.php @@ -23,6 +23,7 @@ use Traversable; use TypeError; use function gettype; +use function is_iterable; use function sprintf; /** @@ -92,7 +93,7 @@ class XMLConverter */ public function convert($records): DOMDocument { - if (!\is_iterable($records)) { + if (!is_iterable($records)) { throw new TypeError(sprintf('%s() expects argument passed to be iterable, %s given', __METHOD__, gettype($records))); } From 9b91560e5c10c436e67d52211e95a2db30a96d6b Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Fri, 7 Sep 2018 21:23:59 +0200 Subject: [PATCH 035/858] bug fix #307 --- CHANGELOG.md | 5 +- src/AbstractCsv.php | 1 + src/Stream.php | 4 ++ src/Writer.php | 108 +++++++++++++++++++++++++++++++++++++++++-- tests/WriterTest.php | 65 ++++++++++++++++++++++++++ 5 files changed, 177 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07d8e4bf..d9bf6741 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,12 @@ All Notable changes to `Csv` will be documented in this file -## 9.1.5 - TBD +## 9.2.0 - TBD ### Added -- Nothing +- `Writer::insertOne` and `Writer::insertAll` take an optional argument specifying + the writing mode. [#307](https://github.com/thephpleague/csv/issues/307) ### Deprecated diff --git a/src/AbstractCsv.php b/src/AbstractCsv.php index 60180475..4a4e8f5d 100644 --- a/src/AbstractCsv.php +++ b/src/AbstractCsv.php @@ -107,6 +107,7 @@ protected function __construct($document) { $this->document = $document; list($this->delimiter, $this->enclosure, $this->escape) = $this->document->getCsvControl(); + $this->resetProperties(); } /** diff --git a/src/Stream.php b/src/Stream.php index 32344f08..4f644366 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -477,6 +477,10 @@ public function fseek(int $offset, int $whence = SEEK_SET) */ public function fwrite(string $str, int $length = 0) { + if (0 === $length) { + return fwrite($this->stream, $str); + } + return fwrite($this->stream, $str, $length); } diff --git a/src/Writer.php b/src/Writer.php index 160b2073..6e8b2ff7 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -35,6 +35,10 @@ */ class Writer extends AbstractCsv { + const MODE_PHP = 'MODE_PHP'; + + const MODE_RFC4180 = 'MODE_RFC4180'; + /** * callable collection to format the record before insertion. * @@ -75,6 +79,28 @@ class Writer extends AbstractCsv */ protected $stream_filter_mode = STREAM_FILTER_WRITE; + /** + * Writer mode. + * + * @var string + */ + protected $writing_mode = self::MODE_PHP; + + /** + * Regular expression used to detect if enclosure are necessary or not. + * + * @var string + */ + protected $rfc4180_regexp; + + /** + * Returns the current writing mode. + */ + public function getWritingMode(): string + { + return $this->writing_mode; + } + /** * Returns the current newline sequence characters. */ @@ -100,7 +126,7 @@ public function getFlushThreshold() * * @param Traversable|array $records */ - public function insertAll($records): int + public function insertAll($records, string $mode = self::MODE_PHP): int { if (!is_iterable($records)) { throw new TypeError(sprintf('%s() expects argument passed to be iterable, %s given', __METHOD__, gettype($records))); @@ -108,7 +134,7 @@ public function insertAll($records): int $bytes = 0; foreach ($records as $record) { - $bytes += $this->insertOne($record); + $bytes += $this->insertOne($record, $mode); } $this->flush_counter = 0; @@ -125,11 +151,16 @@ public function insertAll($records): int * * @throws CannotInsertRecord If the record can not be inserted */ - public function insertOne(array $record): int + public function insertOne(array $record, string $mode = self::MODE_PHP): int { + static $method = [self::MODE_PHP => 'fputcsvPHP', self::MODE_RFC4180 => 'fputcsvRFC4180']; + if (!isset($method[$mode])) { + throw new Exception(sprintf('Unknown or unsupported writing mode %s', $mode)); + } + $record = array_reduce($this->formatters, [$this, 'formatRecord'], $record); $this->validateRecord($record); - $bytes = $this->document->fputcsv($record, $this->delimiter, $this->enclosure, $this->escape); + $bytes = $this->{$method[$mode]}($record); if (false !== $bytes && 0 !== $bytes) { return $bytes + $this->consolidate(); } @@ -137,6 +168,63 @@ public function insertOne(array $record): int throw CannotInsertRecord::triggerOnInsertion($record); } + /** + * Add a single record to a CSV Document using PHP algorithm. + */ + protected function fputcsvPHP(array $record) + { + return $this->document->fputcsv($record, $this->delimiter, $this->enclosure, $this->escape); + } + + /** + * Add a single record to a CSV Document using RFC4180 algorithm. + * + * @throws Exception If the record can not be converted to a string + */ + protected function fputcsvRFC4180(array $record) + { + $retval = []; + foreach ($record as $field) { + if (null === ($content = $this->convertField($field))) { + throw CannotInsertRecord::triggerOnInsertion($record); + } + + $retval[] = $content; + } + + return $this->document->fwrite(implode($this->delimiter, $retval)."\n"); + } + + /** + * Convert and Format a record field to be inserted into a CSV Document. + * + * @return null|string on conversion failure the method returns null + */ + protected function convertField($field) + { + if (null === $field) { + $field = ''; + } + + if ((is_object($field) && !method_exists($field, '__toString')) || !is_scalar($field)) { + return null; + } + + if (is_bool($field)) { + $field = (int) $field; + } + + $field = (string) $field; + if (!preg_match($this->rfc4180_regexp, $field)) { + return $field; + } + + return $this->enclosure + .str_replace($this->enclosure, $this->enclosure.$this->enclosure, $field) + .$this->enclosure + ; + } + /** * Format a record. * @@ -218,6 +306,18 @@ public function setNewline(string $newline): self return $this; } + /** + * Reset dynamic object properties to improve performance. + */ + protected function resetProperties() + { + $this->rfc4180_regexp = "/[\n|\r" + .preg_quote($this->delimiter, '/') + .'|' + .preg_quote($this->enclosure, '/') + .']/'; + } + /** * Set the flush threshold. * diff --git a/tests/WriterTest.php b/tests/WriterTest.php index 5f3259df..1aa5187a 100644 --- a/tests/WriterTest.php +++ b/tests/WriterTest.php @@ -246,4 +246,69 @@ public function testWriterTriggerExceptionWithNonSeekableStream() $writer->setNewline("\r\n"); $writer->insertOne(['foo', 'bar']); } + + /** + * @see https://bugs.php.net/bug.php?id=43225 + * @see https://bugs.php.net/bug.php?id=74713 + * @see https://bugs.php.net/bug.php?id=55413 + * + * @covers ::insertOne + * @covers ::fputcsvRFC4180 + * @covers ::convertField + * + * @dataProvider bugsProvider + */ + public function testRFC4180WriterMode(string $expected, array $record) + { + $csv = Writer::createFromPath('php://temp'); + $csv->setNewline("\r\n"); + $csv->insertOne($record, Writer::MODE_RFC4180); + self::assertSame($expected, $csv->getContent()); + } + + public function bugsProvider() + { + return [ + 'bug #43225' => [ + 'expected' => '"a\""",bbb'."\r\n", + 'record' => ['a\\"', 'bbb'], + ], + 'bug #74713' => [ + 'expected' => '"""@@"",""B"""'."\r\n", + 'record' => ['"@@","B"'], + ], + 'bug #55413' => [ + 'expected' => 'A,"Some ""Stuff""",C'."\r\n", + 'record' => ['A', 'Some "Stuff"', 'C'], + ], + 'convert boolean' => [ + 'expected' => '0,"Some ""Stuff""",C'."\r\n", + 'record' => [false, 'Some "Stuff"', 'C'], + ], + 'convert null value' => [ + 'expected' => ',"Some ""Stuff""",C'."\r\n", + 'record' => [null, 'Some "Stuff"', 'C'], + ], + 'bug #307' => [ + 'expected' => "a text string \\,...\r\n", + 'record' => ['a text string \\', '...'], + ], + ]; + } + + /** + * @see https://bugs.php.net/bug.php?id=43225 + * @see https://bugs.php.net/bug.php?id=74713 + * @see https://bugs.php.net/bug.php?id=55413 + * + * @covers ::insertOne + * @covers ::fputcsvRFC4180 + * @covers ::convertField + */ + public function testRFC4180WriterModeThrowsException() + { + self::expectException(Exception::class); + $csv = Writer::createFromPath('php://temp'); + $csv->insertOne([tmpfile()], Writer::MODE_RFC4180); + } } From 13e2157b64193e079476ea56483ca90ed75e0192 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Fri, 7 Sep 2018 21:35:07 +0200 Subject: [PATCH 036/858] deprecate RFC4180Field class --- CHANGELOG.md | 2 +- src/RFC4180Field.php | 5 +++++ src/Writer.php | 15 --------------- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9bf6741..3803b80b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ All Notable changes to `Csv` will be documented in this file ### Deprecated -- Nothing +- `League\Csv\RFC4180Field` use `Writer::insertXXX` method with the new `Writer::MODE_RFC4180` instead ### Fixed diff --git a/src/RFC4180Field.php b/src/RFC4180Field.php index 6f5f8ada..77db25b1 100644 --- a/src/RFC4180Field.php +++ b/src/RFC4180Field.php @@ -34,6 +34,11 @@ /** * A stream filter to conform the CSV field to RFC4180. * + * DEPRECATION WARNING! This class will be removed in the next major point release + * + * @deprecated deprecated since version 9.2.0 + * @see Writer::insertOne + * * @see https://tools.ietf.org/html/rfc4180#section-2 * * @package League.csv diff --git a/src/Writer.php b/src/Writer.php index 6e8b2ff7..4553ed75 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -79,13 +79,6 @@ class Writer extends AbstractCsv */ protected $stream_filter_mode = STREAM_FILTER_WRITE; - /** - * Writer mode. - * - * @var string - */ - protected $writing_mode = self::MODE_PHP; - /** * Regular expression used to detect if enclosure are necessary or not. * @@ -93,14 +86,6 @@ class Writer extends AbstractCsv */ protected $rfc4180_regexp; - /** - * Returns the current writing mode. - */ - public function getWritingMode(): string - { - return $this->writing_mode; - } - /** * Returns the current newline sequence characters. */ From b49e39d41f00a44a423429a95a773b32d9fb3baf Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Fri, 7 Sep 2018 21:41:26 +0200 Subject: [PATCH 037/858] improve test suite --- .travis.yml | 4 ++-- tests/WriterTest.php | 14 ++++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index f04b472a..477a5563 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,9 +7,9 @@ matrix: - php: 7.0 env: COLLECT_COVERAGE=true VALIDATE_CODING_STYLE=false RUN_PHPSTAN=false IGNORE_PLATFORMS=false - php: 7.1 - env: COLLECT_COVERAGE=true VALIDATE_CODING_STYLE=true RUN_PHPSTAN=true IGNORE_PLATFORMS=false + env: COLLECT_COVERAGE=true VALIDATE_CODING_STYLE=false RUN_PHPSTAN=true IGNORE_PLATFORMS=false - php: 7.2 - env: COLLECT_COVERAGE=true VALIDATE_CODING_STYLE=false RUN_PHPSTAN=false IGNORE_PLATFORMS=false + env: COLLECT_COVERAGE=true VALIDATE_CODING_STYLE=true RUN_PHPSTAN=false IGNORE_PLATFORMS=false - php: nightly env: COLLECT_COVERAGE=false VALIDATE_CODING_STYLE=false RUN_PHPSTAN=false IGNORE_PLATFORMS=true allow_failures: diff --git a/tests/WriterTest.php b/tests/WriterTest.php index 1aa5187a..e452925c 100644 --- a/tests/WriterTest.php +++ b/tests/WriterTest.php @@ -297,10 +297,6 @@ public function bugsProvider() } /** - * @see https://bugs.php.net/bug.php?id=43225 - * @see https://bugs.php.net/bug.php?id=74713 - * @see https://bugs.php.net/bug.php?id=55413 - * * @covers ::insertOne * @covers ::fputcsvRFC4180 * @covers ::convertField @@ -311,4 +307,14 @@ public function testRFC4180WriterModeThrowsException() $csv = Writer::createFromPath('php://temp'); $csv->insertOne([tmpfile()], Writer::MODE_RFC4180); } + + /** + * @covers ::insertOne + */ + public function testRFC4180WriterModeThrowsException2() + { + self::expectException(Exception::class); + $csv = Writer::createFromString(''); + $csv->insertOne(['foo', 'bar'], 'baz'); + } } From 1df649f2108f9b19ef0887114d860f41d3cd9e41 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Sat, 8 Sep 2018 08:06:22 +0200 Subject: [PATCH 038/858] improve test suite --- CHANGELOG.md | 2 ++ src/AbstractCsv.php | 1 - src/Stream.php | 9 +++++---- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3803b80b..67cbe535 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ All Notable changes to `Csv` will be documented in this file ### Fixed - `Writer::insertOne` fix throwing exception when record can not be inserted +- Internal `Stream::fwrite` improved +- Internal `Abstract::__construct` correctly initializes properties ### Removed diff --git a/src/AbstractCsv.php b/src/AbstractCsv.php index 4a4e8f5d..09b4227d 100644 --- a/src/AbstractCsv.php +++ b/src/AbstractCsv.php @@ -297,7 +297,6 @@ public function getContent(): string /** * Outputs all data on the CSV file. * - * @param null|string $filename * * @return int Returns the number of characters read from the handle * and passed through to the output. diff --git a/src/Stream.php b/src/Stream.php index 4f644366..402df3e3 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -475,13 +475,14 @@ public function fseek(int $offset, int $whence = SEEK_SET) * * @return int|bool */ - public function fwrite(string $str, int $length = 0) + public function fwrite(string $str, int $length = null) { - if (0 === $length) { - return fwrite($this->stream, $str); + $args = [$this->stream, $str]; + if (null !== $length) { + $args[] = $length; } - return fwrite($this->stream, $str, $length); + return fwrite(...$args); } /** From a0bc1b4cdc56c6e5bbfe4f72969133d189c06af9 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Mon, 10 Sep 2018 09:49:40 +0200 Subject: [PATCH 039/858] Improve field conversion in RFC4180 mode --- src/Writer.php | 38 +++++++++----------------------------- tests/WriterTest.php | 14 +------------- 2 files changed, 10 insertions(+), 42 deletions(-) diff --git a/src/Writer.php b/src/Writer.php index 4553ed75..8c01d6ef 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -20,10 +20,13 @@ use TypeError; use const SEEK_CUR; use const STREAM_FILTER_WRITE; +use function array_map; use function array_reduce; use function gettype; +use function implode; use function is_iterable; use function sprintf; +use function str_replace; use function strlen; /** @@ -154,7 +157,7 @@ public function insertOne(array $record, string $mode = self::MODE_PHP): int } /** - * Add a single record to a CSV Document using PHP algorithm. + * Adds a single record to a CSV Document using PHP algorithm. */ protected function fputcsvPHP(array $record) { @@ -162,43 +165,20 @@ protected function fputcsvPHP(array $record) } /** - * Add a single record to a CSV Document using RFC4180 algorithm. - * - * @throws Exception If the record can not be converted to a string + * Adds a single record to a CSV Document using RFC4180 algorithm. */ protected function fputcsvRFC4180(array $record) { - $retval = []; - foreach ($record as $field) { - if (null === ($content = $this->convertField($field))) { - throw CannotInsertRecord::triggerOnInsertion($record); - } - - $retval[] = $content; - } - - return $this->document->fwrite(implode($this->delimiter, $retval)."\n"); + return $this->document->fwrite(implode($this->delimiter, array_map([$this, 'convertField'], $record))."\n"); } /** - * Convert and Format a record field to be inserted into a CSV Document. + * Converts and Format a record field to be inserted into a CSV Document. * - * @return null|string on conversion failure the method returns null + * @see https://tools.ietf.org/html/rfc4180 */ - protected function convertField($field) + protected function convertField($field): string { - if (null === $field) { - $field = ''; - } - - if ((is_object($field) && !method_exists($field, '__toString')) || !is_scalar($field)) { - return null; - } - - if (is_bool($field)) { - $field = (int) $field; - } - $field = (string) $field; if (!preg_match($this->rfc4180_regexp, $field)) { return $field; diff --git a/tests/WriterTest.php b/tests/WriterTest.php index e452925c..7d3cd826 100644 --- a/tests/WriterTest.php +++ b/tests/WriterTest.php @@ -282,7 +282,7 @@ public function bugsProvider() 'record' => ['A', 'Some "Stuff"', 'C'], ], 'convert boolean' => [ - 'expected' => '0,"Some ""Stuff""",C'."\r\n", + 'expected' => ',"Some ""Stuff""",C'."\r\n", 'record' => [false, 'Some "Stuff"', 'C'], ], 'convert null value' => [ @@ -296,18 +296,6 @@ public function bugsProvider() ]; } - /** - * @covers ::insertOne - * @covers ::fputcsvRFC4180 - * @covers ::convertField - */ - public function testRFC4180WriterModeThrowsException() - { - self::expectException(Exception::class); - $csv = Writer::createFromPath('php://temp'); - $csv->insertOne([tmpfile()], Writer::MODE_RFC4180); - } - /** * @covers ::insertOne */ From 364b462702380e0de4e353b10ec03cdc3d21edb7 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Mon, 10 Sep 2018 14:33:09 +0200 Subject: [PATCH 040/858] improve RFC4180 compliance --- src/Reader.php | 1 + src/Writer.php | 51 +++++++++++++++++++++++++++++--------------- tests/WriterTest.php | 12 +++++++++++ 3 files changed, 47 insertions(+), 17 deletions(-) diff --git a/src/Reader.php b/src/Reader.php index a6a85df7..b551c4c2 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -365,6 +365,7 @@ public function setHeaderOffset($offset): self */ protected function resetProperties() { + parent::resetProperties(); $this->nb_records = -1; $this->header = []; } diff --git a/src/Writer.php b/src/Writer.php index 8c01d6ef..fa59da61 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -83,12 +83,19 @@ class Writer extends AbstractCsv protected $stream_filter_mode = STREAM_FILTER_WRITE; /** - * Regular expression used to detect if enclosure are necessary or not. + * Regular expression used to detect if RFC4180 formatting is necessary. * * @var string */ protected $rfc4180_regexp; + /** + * double enclosure for RFC4180 compliance. + * + * @var string + */ + protected $rfc4180_enclosure; + /** * Returns the current newline sequence characters. */ @@ -176,6 +183,16 @@ protected function fputcsvRFC4180(array $record) * Converts and Format a record field to be inserted into a CSV Document. * * @see https://tools.ietf.org/html/rfc4180 + * @see http://edoceo.com/utilitas/csv-file-format + * + * - string conversion is done without any check like fputcsv + * + * Enclosure addition or doubling is done using the following rules + * + * - Preserving Leading and trailing whitespaces - Fields must be delimited with enclosure. + * - Embedded delimiter - Fields must be delimited with enclosure. + * - Embedded enclosure - Fields must be delimited with enclosure, embedded enclosures must be doubled. + * - Embedded line-breaks - Fields must be delimited with enclosure. */ protected function convertField($field): string { @@ -184,10 +201,22 @@ protected function convertField($field): string return $field; } - return $this->enclosure - .str_replace($this->enclosure, $this->enclosure.$this->enclosure, $field) - .$this->enclosure - ; + return $this->enclosure.str_replace($this->enclosure, $this->rfc4180_enclosure, $field).$this->enclosure; + } + + /** + * Reset dynamic object properties to improve performance. + */ + protected function resetProperties() + { + parent::resetProperties(); + $this->rfc4180_regexp = "/^ + (\ +) + |([\n|\r".preg_quote($this->delimiter, '/').'|'.preg_quote($this->enclosure, '/').']) + |(\ +) + $/x'; + + $this->rfc4180_enclosure = $this->enclosure.$this->enclosure; } /** @@ -271,18 +300,6 @@ public function setNewline(string $newline): self return $this; } - /** - * Reset dynamic object properties to improve performance. - */ - protected function resetProperties() - { - $this->rfc4180_regexp = "/[\n|\r" - .preg_quote($this->delimiter, '/') - .'|' - .preg_quote($this->enclosure, '/') - .']/'; - } - /** * Set the flush threshold. * diff --git a/tests/WriterTest.php b/tests/WriterTest.php index 7d3cd826..22106855 100644 --- a/tests/WriterTest.php +++ b/tests/WriterTest.php @@ -293,6 +293,18 @@ public function bugsProvider() 'expected' => "a text string \\,...\r\n", 'record' => ['a text string \\', '...'], ], + 'line starting with space' => [ + 'expected' => '" a",foo,bar'."\r\n", + 'record' => [' a', 'foo', 'bar'], + ], + 'line ending with space' => [ + 'expected' => 'a,foo,"bar "'."\r\n", + 'record' => ['a', 'foo', 'bar '], + ], + 'line containing space' => [ + 'expected' => 'a,foo bar,bar'."\r\n", + 'record' => ['a', 'foo bar', 'bar'], + ], ]; } From 5c61e334943f67dda12e31196b3d15bc4b8d7e41 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Mon, 10 Sep 2018 17:14:43 +0200 Subject: [PATCH 041/858] Improve RFC4180 implementation --- .travis.yml | 4 +-- src/Stream.php | 2 +- src/Writer.php | 73 ++++++++++++++++++++++++++------------------ tests/WriterTest.php | 4 +-- 4 files changed, 48 insertions(+), 35 deletions(-) diff --git a/.travis.yml b/.travis.yml index 477a5563..f04b472a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,9 +7,9 @@ matrix: - php: 7.0 env: COLLECT_COVERAGE=true VALIDATE_CODING_STYLE=false RUN_PHPSTAN=false IGNORE_PLATFORMS=false - php: 7.1 - env: COLLECT_COVERAGE=true VALIDATE_CODING_STYLE=false RUN_PHPSTAN=true IGNORE_PLATFORMS=false + env: COLLECT_COVERAGE=true VALIDATE_CODING_STYLE=true RUN_PHPSTAN=true IGNORE_PLATFORMS=false - php: 7.2 - env: COLLECT_COVERAGE=true VALIDATE_CODING_STYLE=true RUN_PHPSTAN=false IGNORE_PLATFORMS=false + env: COLLECT_COVERAGE=true VALIDATE_CODING_STYLE=false RUN_PHPSTAN=false IGNORE_PLATFORMS=false - php: nightly env: COLLECT_COVERAGE=false VALIDATE_CODING_STYLE=false RUN_PHPSTAN=false IGNORE_PLATFORMS=true allow_failures: diff --git a/src/Stream.php b/src/Stream.php index 402df3e3..ad8ac6cc 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -300,7 +300,7 @@ public function setFlags(int $flags) * * @see http://php.net/manual/en/splfileobject.fputcsv.php * - * @return int|null|bool + * @return int|bool */ public function fputcsv(array $fields, string $delimiter = ',', string $enclosure = '"', string $escape = '\\') { diff --git a/src/Writer.php b/src/Writer.php index fa59da61..ea2be59b 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -20,7 +20,6 @@ use TypeError; use const SEEK_CUR; use const STREAM_FILTER_WRITE; -use function array_map; use function array_reduce; use function gettype; use function implode; @@ -148,14 +147,15 @@ public function insertAll($records, string $mode = self::MODE_PHP): int */ public function insertOne(array $record, string $mode = self::MODE_PHP): int { - static $method = [self::MODE_PHP => 'fputcsvPHP', self::MODE_RFC4180 => 'fputcsvRFC4180']; - if (!isset($method[$mode])) { + static $methodList = [self::MODE_PHP => 'addRecord', self::MODE_RFC4180 => 'addRFC4180CompliantRecord']; + $method = $methodList[$mode] ?? null; + if (null === $method) { throw new Exception(sprintf('Unknown or unsupported writing mode %s', $mode)); } $record = array_reduce($this->formatters, [$this, 'formatRecord'], $record); $this->validateRecord($record); - $bytes = $this->{$method[$mode]}($record); + $bytes = $this->$method($record); if (false !== $bytes && 0 !== $bytes) { return $bytes + $this->consolidate(); } @@ -165,57 +165,70 @@ public function insertOne(array $record, string $mode = self::MODE_PHP): int /** * Adds a single record to a CSV Document using PHP algorithm. + * + * @see https://php.net/manual/en/function.fputcsv.php + * + * @return int|bool */ - protected function fputcsvPHP(array $record) + protected function addRecord(array $record) { return $this->document->fputcsv($record, $this->delimiter, $this->enclosure, $this->escape); } /** * Adds a single record to a CSV Document using RFC4180 algorithm. - */ - protected function fputcsvRFC4180(array $record) - { - return $this->document->fwrite(implode($this->delimiter, array_map([$this, 'convertField'], $record))."\n"); - } - - /** - * Converts and Format a record field to be inserted into a CSV Document. * + * @see https://php.net/manual/en/function.fputcsv.php + * @see https://php.net/manual/en/function.fwrite.php * @see https://tools.ietf.org/html/rfc4180 * @see http://edoceo.com/utilitas/csv-file-format * - * - string conversion is done without any check like fputcsv + * Fields must be delimited with enclosures if they contains : * - * Enclosure addition or doubling is done using the following rules + * - Leading or trailing whitespaces + * - Embedded delimiters + * - Embedded line-breaks + * - Embedded enclosures. * - * - Preserving Leading and trailing whitespaces - Fields must be delimited with enclosure. - * - Embedded delimiter - Fields must be delimited with enclosure. - * - Embedded enclosure - Fields must be delimited with enclosure, embedded enclosures must be doubled. - * - Embedded line-breaks - Fields must be delimited with enclosure. + * Embedded enclosures must be doubled. + * + * String conversion is done without any check like fputcsv: + * + * - Emits E_NOTICE on Array conversion (returns the 'Array' string) + * - Throws catchable fatal error on objects that can not be converted + * - Returns resource id without notice or error (returns 'Resource id #2') + * - Converts boolean true to '1', boolean false to the empty string + * - Converts null value to the empty string + * + * the LF character is added at the end of each record to mimic fputcsv behavior + * + * @return int|bool */ - protected function convertField($field): string + protected function addRFC4180CompliantRecord(array $record) { - $field = (string) $field; - if (!preg_match($this->rfc4180_regexp, $field)) { - return $field; + foreach ($record as &$field) { + $field = (string) $field; + if (preg_match($this->rfc4180_regexp, $field)) { + $field = $this->enclosure.str_replace($this->enclosure, $this->rfc4180_enclosure, $field).$this->enclosure; + } } + unset($field); - return $this->enclosure.str_replace($this->enclosure, $this->rfc4180_enclosure, $field).$this->enclosure; + return $this->document->fwrite(implode($this->delimiter, $record)."\n"); } /** - * Reset dynamic object properties to improve performance. + * {@inheritdoc} */ protected function resetProperties() { parent::resetProperties(); - $this->rfc4180_regexp = "/^ - (\ +) - |([\n|\r".preg_quote($this->delimiter, '/').'|'.preg_quote($this->enclosure, '/').']) - |(\ +) + $embedded_characters = "\n|\r".preg_quote($this->delimiter, '/').'|'.preg_quote($this->enclosure, '/'); + $this->rfc4180_regexp = '/^ + (\ +) # leading whitespaces + |(['.$embedded_characters.']) # embedded characters + |(\ +) # trailing whitespaces $/x'; - $this->rfc4180_enclosure = $this->enclosure.$this->enclosure; } diff --git a/tests/WriterTest.php b/tests/WriterTest.php index 22106855..46e0f920 100644 --- a/tests/WriterTest.php +++ b/tests/WriterTest.php @@ -115,6 +115,7 @@ public function testInsert() /** * @covers ::insertOne + * @covers ::addRecord */ public function testInsertNormalFile() { @@ -253,8 +254,7 @@ public function testWriterTriggerExceptionWithNonSeekableStream() * @see https://bugs.php.net/bug.php?id=55413 * * @covers ::insertOne - * @covers ::fputcsvRFC4180 - * @covers ::convertField + * @covers ::addRFC4180CompliantRecord * * @dataProvider bugsProvider */ From 157a9dd60c4bc3ce7c286733dd89d9c36e4478bf Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Tue, 11 Sep 2018 12:29:02 +0200 Subject: [PATCH 042/858] update v9 documentation --- composer.json | 5 ++- docs/9.0/interoperability/rfc4180-field.md | 2 ++ docs/9.0/writer/index.md | 41 ++++++++++++++++++---- src/Writer.php | 30 ++++++++-------- 4 files changed, 56 insertions(+), 22 deletions(-) diff --git a/composer.json b/composer.json index fe9bce12..8072abce 100644 --- a/composer.json +++ b/composer.json @@ -14,8 +14,11 @@ } ], "support": { + "docs": "https://csv.thephpleague.com/", "forum": "https://groups.google.com/forum/#!forum/thephpleague", - "issues": "https://github.com/thephpleague/csv/issues" + "issues": "https://github.com/thephpleague/csv/issues", + "rss": "https://github.com/thephpleague/csv/releases.atom", + "source": "https://github.com/thephpleague/csv" }, "require": { "php" : ">=7.0.10", diff --git a/docs/9.0/interoperability/rfc4180-field.md b/docs/9.0/interoperability/rfc4180-field.md index f0f3882a..1a59ed70 100644 --- a/docs/9.0/interoperability/rfc4180-field.md +++ b/docs/9.0/interoperability/rfc4180-field.md @@ -5,6 +5,8 @@ title: CSV document interoperability # RFC4180 Field compliance +

This class is deprecated as of version 9.2.0. Please use directly the Writer::insertXXX methods with the Writer::MODE_RFC4180 argument instead.

+ ~~~php Starting with 9.2.0 both methods can have a optional second argument to define which algorithm to use to insert the record array

+ +- `Writer::MODE_PHP` uses directly PHP `fputcsv` (this is the default behaviour) +- `Writer::MODE_RFC4180` produces a record line compliant with the informal [RFC4180](https://tools.ietf.org/html/rfc4180) specification. + +~~~php +insertOne(['"foo"', 'foo bar', 'baz ', 'foo\\bar']); +$writer->insertOne(['"foo"', 'foo bar', 'baz ', 'foo\\bar'], Writer::MODE_RFC4180); +echo $writer->getContents(); +// """foo""","foo bar","baz ","foo\bar" +// """foo""",foo bar,"baz ",foo\bar +~~~ + +

The addition of this new argument means the deprecation of [RFC4180Field](/9.0/interoperability/rfc4180-field/).

+ +

You still need to set the newline sequence as shown below

+ ## Handling newline Because PHP's `fputcsv` implementation has a hardcoded `\n`, we need to be able to replace the last `LF` code with one supplied by the developer for more interoperability between CSV packages on different platforms. The newline sequence will be appended to each newly inserted CSV record. diff --git a/src/Writer.php b/src/Writer.php index ea2be59b..b7a6abbd 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -183,6 +183,14 @@ protected function addRecord(array $record) * @see https://tools.ietf.org/html/rfc4180 * @see http://edoceo.com/utilitas/csv-file-format * + * String conversion is done without any check like fputcsv. + * + * - Emits E_NOTICE on Array conversion (returns the 'Array' string) + * - Throws catchable fatal error on objects that can not be converted + * - Returns resource id without notice or error (returns 'Resource id #2') + * - Converts boolean true to '1', boolean false to the empty string + * - Converts null value to the empty string + * * Fields must be delimited with enclosures if they contains : * * - Leading or trailing whitespaces @@ -192,15 +200,7 @@ protected function addRecord(array $record) * * Embedded enclosures must be doubled. * - * String conversion is done without any check like fputcsv: - * - * - Emits E_NOTICE on Array conversion (returns the 'Array' string) - * - Throws catchable fatal error on objects that can not be converted - * - Returns resource id without notice or error (returns 'Resource id #2') - * - Converts boolean true to '1', boolean false to the empty string - * - Converts null value to the empty string - * - * the LF character is added at the end of each record to mimic fputcsv behavior + * The LF character is added at the end of each record to mimic fputcsv behavior * * @return int|bool */ @@ -223,12 +223,12 @@ protected function addRFC4180CompliantRecord(array $record) protected function resetProperties() { parent::resetProperties(); - $embedded_characters = "\n|\r".preg_quote($this->delimiter, '/').'|'.preg_quote($this->enclosure, '/'); - $this->rfc4180_regexp = '/^ - (\ +) # leading whitespaces - |(['.$embedded_characters.']) # embedded characters - |(\ +) # trailing whitespaces - $/x'; + $characters = "\n|\r".preg_quote($this->delimiter, '/').'|'.preg_quote($this->enclosure, '/'); + $this->rfc4180_regexp = '/ + ^(\ +) # leading whitespaces + |(['.$characters.']) # delimiter, enclosure, line-breaks characters + |(\ +)$ # trailing whitespaces + /x'; $this->rfc4180_enclosure = $this->enclosure.$this->enclosure; } From da27840ee7050f08d4751a791f253e801e32886c Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Thu, 13 Sep 2018 18:10:38 +0200 Subject: [PATCH 043/858] implementing PR as polyfill for fputcsv/fgetcsv fix --- src/AbstractCsv.php | 50 ++++++++++++++++++++++++++++++++++---------- src/Reader.php | 24 ++++++++++----------- src/Writer.php | 47 +++++++++++++++++++---------------------- tests/CsvTest.php | 19 +++++++++++++++++ tests/WriterTest.php | 19 ++++++----------- 5 files changed, 97 insertions(+), 62 deletions(-) diff --git a/src/AbstractCsv.php b/src/AbstractCsv.php index 09b4227d..790a1091 100644 --- a/src/AbstractCsv.php +++ b/src/AbstractCsv.php @@ -91,6 +91,12 @@ abstract class AbstractCsv implements ByteSequence */ protected $escape = '\\'; + /** + * Does (fput|fget)csv supports the empty string escape. + * @var bool + */ + protected static $has_native_support_for_empty_string_escape_char; + /** * The CSV document. * @@ -110,6 +116,18 @@ protected function __construct($document) $this->resetProperties(); } + /** + * Reset dynamic object properties to improve performance. + */ + protected function resetProperties() + { + if (null === static::$has_native_support_for_empty_string_escape_char) { + //This should be replace by a polyfill targetting PHP_VERSION constant + $res = @fgetcsv(tmpfile(), 0, ',', '"', ''); + static::$has_native_support_for_empty_string_escape_char = false !== $res; + } + } + /** * {@inheritdoc} */ @@ -153,7 +171,7 @@ public static function createFromStream($stream) * * @return static */ - public static function createFromString(string $content) + public static function createFromString(string $content = '') { return new static(Stream::createFromString($content)); } @@ -194,6 +212,23 @@ public function getEscape(): string return $this->escape; } + /** + * Computes the escape character. + * + * If the escape character is the empty string and the native fgetcsv and fputcsv + * do not supports the empty string it is replaced by the null byte character. + * + * THIS HACK may not work if the CSV content contains a null byte character. + */ + protected function getEscapeChar(): string + { + if ('' === $this->escape && !self::$has_native_support_for_empty_string_escape_char) { + return "\0"; + } + + return $this->escape; + } + /** * Returns the BOM sequence in use on Output methods. */ @@ -212,7 +247,7 @@ public function getInputBOM(): string } $this->document->setFlags(SplFileObject::READ_CSV); - $this->document->setCsvControl($this->delimiter, $this->enclosure, $this->escape); + $this->document->setCsvControl($this->delimiter, $this->enclosure, $this->getEscapeChar()); $this->document->rewind(); $this->input_bom = bom_match(implode(',', (array) $this->document->current())); @@ -370,13 +405,6 @@ public function setDelimiter(string $delimiter): self throw new Exception(sprintf('%s() expects delimiter to be a single character %s given', __METHOD__, $delimiter)); } - /** - * Reset dynamic object properties to improve performance. - */ - protected function resetProperties() - { - } - /** * Sets the field enclosure. * @@ -413,14 +441,14 @@ public function setEscape(string $escape): self return $this; } - if (1 === strlen($escape)) { + if ('' === $escape || 1 === strlen($escape)) { $this->escape = $escape; $this->resetProperties(); return $this; } - throw new Exception(sprintf('%s() expects escape to be a single character %s given', __METHOD__, $escape)); + throw new Exception(sprintf('%s() expects escape to be the empty string or single character %s given', __METHOD__, $escape)); } /** diff --git a/src/Reader.php b/src/Reader.php index b551c4c2..cd3f438c 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -86,6 +86,16 @@ public static function createFromPath(string $path, string $open_mode = 'r', $co return new static(Stream::createFromPath($path, $open_mode, $context)); } + /** + * {@inheritdoc} + */ + protected function resetProperties() + { + parent::resetProperties(); + $this->nb_records = -1; + $this->header = []; + } + /** * Returns the header offset. * @@ -147,7 +157,7 @@ protected function setHeader(int $offset): array protected function seekRow(int $offset) { $this->document->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY); - $this->document->setCsvControl($this->delimiter, $this->enclosure, $this->escape); + $this->document->setCsvControl($this->delimiter, $this->enclosure, $this->getEscapeChar()); $this->document->rewind(); //Workaround for SplFileObject::seek bug in PHP7.2+ see https://bugs.php.net/bug.php?id=75917 @@ -250,7 +260,7 @@ public function getRecords(array $header = []): Iterator }; $bom = $this->getInputBOM(); $this->document->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY); - $this->document->setCsvControl($this->delimiter, $this->enclosure, $this->escape); + $this->document->setCsvControl($this->delimiter, $this->enclosure, $this->getEscapeChar()); $records = $this->stripBOM(new CallbackFilterIterator($this->document, $normalized), $bom); if (null !== $this->header_offset) { @@ -359,14 +369,4 @@ public function setHeaderOffset($offset): self return $this; } - - /** - * {@inheritdoc} - */ - protected function resetProperties() - { - parent::resetProperties(); - $this->nb_records = -1; - $this->header = []; - } } diff --git a/src/Writer.php b/src/Writer.php index b7a6abbd..7f312a19 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -37,10 +37,6 @@ */ class Writer extends AbstractCsv { - const MODE_PHP = 'MODE_PHP'; - - const MODE_RFC4180 = 'MODE_RFC4180'; - /** * callable collection to format the record before insertion. * @@ -95,6 +91,21 @@ class Writer extends AbstractCsv */ protected $rfc4180_enclosure; + /** + * {@inheritdoc} + */ + protected function resetProperties() + { + parent::resetProperties(); + $characters = "\n|\r".preg_quote($this->delimiter, '/').'|'.preg_quote($this->enclosure, '/'); + $this->rfc4180_regexp = '/ + ^(\ +) # leading whitespaces + |(['.$characters.']) # delimiter, enclosure, line-breaks characters + |(\ +)$ # trailing whitespaces + /x'; + $this->rfc4180_enclosure = $this->enclosure.$this->enclosure; + } + /** * Returns the current newline sequence characters. */ @@ -120,7 +131,7 @@ public function getFlushThreshold() * * @param Traversable|array $records */ - public function insertAll($records, string $mode = self::MODE_PHP): int + public function insertAll($records): int { if (!is_iterable($records)) { throw new TypeError(sprintf('%s() expects argument passed to be iterable, %s given', __METHOD__, gettype($records))); @@ -128,7 +139,7 @@ public function insertAll($records, string $mode = self::MODE_PHP): int $bytes = 0; foreach ($records as $record) { - $bytes += $this->insertOne($record, $mode); + $bytes += $this->insertOne($record); } $this->flush_counter = 0; @@ -145,12 +156,11 @@ public function insertAll($records, string $mode = self::MODE_PHP): int * * @throws CannotInsertRecord If the record can not be inserted */ - public function insertOne(array $record, string $mode = self::MODE_PHP): int + public function insertOne(array $record): int { - static $methodList = [self::MODE_PHP => 'addRecord', self::MODE_RFC4180 => 'addRFC4180CompliantRecord']; - $method = $methodList[$mode] ?? null; - if (null === $method) { - throw new Exception(sprintf('Unknown or unsupported writing mode %s', $mode)); + $method = 'addRecord'; + if ('' === $this->escape && !static::$has_native_support_for_empty_string_escape_char) { + $method = 'addRFC4180CompliantRecord'; } $record = array_reduce($this->formatters, [$this, 'formatRecord'], $record); @@ -217,21 +227,6 @@ protected function addRFC4180CompliantRecord(array $record) return $this->document->fwrite(implode($this->delimiter, $record)."\n"); } - /** - * {@inheritdoc} - */ - protected function resetProperties() - { - parent::resetProperties(); - $characters = "\n|\r".preg_quote($this->delimiter, '/').'|'.preg_quote($this->enclosure, '/'); - $this->rfc4180_regexp = '/ - ^(\ +) # leading whitespaces - |(['.$characters.']) # delimiter, enclosure, line-breaks characters - |(\ +)$ # trailing whitespaces - /x'; - $this->rfc4180_enclosure = $this->enclosure.$this->enclosure; - } - /** * Format a record. * diff --git a/tests/CsvTest.php b/tests/CsvTest.php index 40e63754..a940dc86 100644 --- a/tests/CsvTest.php +++ b/tests/CsvTest.php @@ -18,6 +18,7 @@ use League\Csv\Reader; use League\Csv\Writer; use PHPUnit\Framework\TestCase; +use ReflectionClass; use SplTempFileObject; use const PHP_EOL; use const PHP_VERSION; @@ -63,6 +64,7 @@ public function tearDown() /** * @covers ::createFromFileObject + * @covers ::__construct */ public function testCreateFromFileObjectPreserveFileObjectCsvControls() { @@ -421,4 +423,21 @@ public function testIsIterablePolyFill() self::assertFalse(is_iterable((object) ['foo'])); self::assertFalse(is_iterable(Writer::createFromString(''))); } + + /** + * @covers ::setDelimiter + * @covers ::resetProperties + */ + public function testResetProperties() + { + $csv = Writer::createFromString(); + $csv->setDelimiter('|'); + $ref = new ReflectionClass($csv); + $prop = $ref->getProperty('has_native_support_for_empty_string_escape_char'); + $prop->setAccessible(true); + $prop->setValue(null); + self::assertNull($prop->getValue()); + $csv->setDelimiter(';'); + self::assertFalse($prop->getValue()); + } } diff --git a/tests/WriterTest.php b/tests/WriterTest.php index 46e0f920..d6a42b45 100644 --- a/tests/WriterTest.php +++ b/tests/WriterTest.php @@ -253,20 +253,23 @@ public function testWriterTriggerExceptionWithNonSeekableStream() * @see https://bugs.php.net/bug.php?id=74713 * @see https://bugs.php.net/bug.php?id=55413 * + * @covers ::getInputBOM + * @covers ::getEscapeChar * @covers ::insertOne * @covers ::addRFC4180CompliantRecord * - * @dataProvider bugsProvider + * @dataProvider compliantRFC4180Provider */ public function testRFC4180WriterMode(string $expected, array $record) { $csv = Writer::createFromPath('php://temp'); $csv->setNewline("\r\n"); - $csv->insertOne($record, Writer::MODE_RFC4180); + $csv->setEscape(''); + $csv->insertOne($record); self::assertSame($expected, $csv->getContent()); } - public function bugsProvider() + public function compliantRFC4180Provider() { return [ 'bug #43225' => [ @@ -307,14 +310,4 @@ public function bugsProvider() ], ]; } - - /** - * @covers ::insertOne - */ - public function testRFC4180WriterModeThrowsException2() - { - self::expectException(Exception::class); - $csv = Writer::createFromString(''); - $csv->insertOne(['foo', 'bar'], 'baz'); - } } From 2ac8322980727ce1b3aa8ef1510e1e0f7d87514b Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Thu, 13 Sep 2018 18:19:01 +0200 Subject: [PATCH 044/858] update documentation for v9.2 --- CHANGELOG.md | 8 ++++---- docs/9.0/connections/controls.md | 12 +++++++++++- docs/9.0/interoperability/rfc4180-field.md | 2 +- docs/9.0/writer/index.md | 20 ++++++++------------ 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67cbe535..ee1ed2c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,16 +6,16 @@ All Notable changes to `Csv` will be documented in this file ### Added -- `Writer::insertOne` and `Writer::insertAll` take an optional argument specifying - the writing mode. [#307](https://github.com/thephpleague/csv/issues/307) +- Supports for empty string for the escape character to enable better RFC4180 compliance. ### Deprecated -- `League\Csv\RFC4180Field` use `Writer::insertXXX` method with the new `Writer::MODE_RFC4180` instead +- `League\Csv\RFC4180Field` use `Writer::insertXXX` methods instead. ### Fixed -- `Writer::insertOne` fix throwing exception when record can not be inserted +- `AbstractCSV::setEscape` now accepts the empty string like `fputcsv` and `fgetcsv` +- `Writer::insertOne` fixes throwing exception when record can not be inserted - Internal `Stream::fwrite` improved - Internal `Abstract::__construct` correctly initializes properties diff --git a/docs/9.0/connections/controls.md b/docs/9.0/connections/controls.md index 533685be..49dae818 100644 --- a/docs/9.0/connections/controls.md +++ b/docs/9.0/connections/controls.md @@ -85,7 +85,17 @@ $escape = $csv->getEscape(); //returns "\"

The default escape character is \.

-

To produce RFC4180 compliant CSV documents you should use the RFC4180Field stream filter to work around bugs associated with the use of the escape character.

+

Starting with 9.2.0 you can provide an empty string for the escape character to enable better RFC4180 compliance.

+ +~~~php +setEscape(''); +$escape = $csv->getEscape(); //returns "" +~~~ ## Inherited character controls diff --git a/docs/9.0/interoperability/rfc4180-field.md b/docs/9.0/interoperability/rfc4180-field.md index 1a59ed70..11d87f30 100644 --- a/docs/9.0/interoperability/rfc4180-field.md +++ b/docs/9.0/interoperability/rfc4180-field.md @@ -5,7 +5,7 @@ title: CSV document interoperability # RFC4180 Field compliance -

This class is deprecated as of version 9.2.0. Please use directly the Writer::insertXXX methods with the Writer::MODE_RFC4180 argument instead.

+

This class is deprecated as of version 9.2.0. Please use directly the Writer::insertXXX methods with the empty escape character argument instead.

~~~php Starting with 9.2.0 both methods can have a optional second argument to define which algorithm to use to insert the record array

- -- `Writer::MODE_PHP` uses directly PHP `fputcsv` (this is the default behaviour) -- `Writer::MODE_RFC4180` produces a record line compliant with the informal [RFC4180](https://tools.ietf.org/html/rfc4180) specification. +

Starting with 9.2.0 if you can provide an empty string for the escape character to enable better RFC4180 compliance.

~~~php insertOne(['"foo"', 'foo bar', 'baz ', 'foo\\bar']); -$writer->insertOne(['"foo"', 'foo bar', 'baz ', 'foo\\bar'], Writer::MODE_RFC4180); +$writer->setEscape(''); +$writer->insertOne(['"foo"', 'foo bar', 'baz ', 'foo\\bar']); echo $writer->getContents(); // """foo""","foo bar","baz ","foo\bar" // """foo""",foo bar,"baz ",foo\bar ~~~ -

The addition of this new argument means the deprecation of [RFC4180Field](/9.0/interoperability/rfc4180-field/).

+

The addition of this new feature means the deprecation of [RFC4180Field](/9.0/interoperability/rfc4180-field/).

You still need to set the newline sequence as shown below

From 9683ce7881ccbcfd98c5b117541e9ed427bf9a7a Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Thu, 13 Sep 2018 21:46:15 +0200 Subject: [PATCH 045/858] match RFC compliance to fputcsv behaviout without escape character --- src/Writer.php | 8 ++------ tests/WriterTest.php | 4 ++-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Writer.php b/src/Writer.php index 7f312a19..e7969068 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -97,12 +97,8 @@ class Writer extends AbstractCsv protected function resetProperties() { parent::resetProperties(); - $characters = "\n|\r".preg_quote($this->delimiter, '/').'|'.preg_quote($this->enclosure, '/'); - $this->rfc4180_regexp = '/ - ^(\ +) # leading whitespaces - |(['.$characters.']) # delimiter, enclosure, line-breaks characters - |(\ +)$ # trailing whitespaces - /x'; + $characters = "\s|".preg_quote($this->delimiter, '/').'|'.preg_quote($this->enclosure, '/'); + $this->rfc4180_regexp = '/['.$characters.']/x'; $this->rfc4180_enclosure = $this->enclosure.$this->enclosure; } diff --git a/tests/WriterTest.php b/tests/WriterTest.php index d6a42b45..29a4dbed 100644 --- a/tests/WriterTest.php +++ b/tests/WriterTest.php @@ -293,7 +293,7 @@ public function compliantRFC4180Provider() 'record' => [null, 'Some "Stuff"', 'C'], ], 'bug #307' => [ - 'expected' => "a text string \\,...\r\n", + 'expected' => '"a text string \\",...'."\r\n", 'record' => ['a text string \\', '...'], ], 'line starting with space' => [ @@ -305,7 +305,7 @@ public function compliantRFC4180Provider() 'record' => ['a', 'foo', 'bar '], ], 'line containing space' => [ - 'expected' => 'a,foo bar,bar'."\r\n", + 'expected' => 'a,"foo bar",bar'."\r\n", 'record' => ['a', 'foo bar', 'bar'], ], ]; From 24cbbc62b7d290d4f2861b8931f2a3a9816002dd Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Fri, 14 Sep 2018 12:43:13 +0200 Subject: [PATCH 046/858] Adding a polyfill for the Reader class The polyfill for the Reader class uses a RFC418 Parser Iterator which is used only if the escape character is the empty string and on a PHP version where fgetcsv does not support the empty escape string see https://github.com/php/php-src/pull/3515 for more info on when the patch will land on PHP master trunk and when it will be available --- phpstan.tests.neon | 1 + src/AbstractCsv.php | 32 +------ src/RFC4180Field.php | 2 +- src/RFC4180Iterator.php | 164 ++++++++++++++++++++++++++++++++++ src/Reader.php | 41 +++++---- src/Stream.php | 13 +++ src/Writer.php | 4 +- tests/CsvTest.php | 18 ---- tests/RFC4180IteratorTest.php | 75 ++++++++++++++++ tests/ReaderTest.php | 33 +++++++ tests/WriterTest.php | 1 - 11 files changed, 316 insertions(+), 68 deletions(-) create mode 100644 src/RFC4180Iterator.php create mode 100644 tests/RFC4180IteratorTest.php diff --git a/phpstan.tests.neon b/phpstan.tests.neon index 19af9bb2..11046dd2 100644 --- a/phpstan.tests.neon +++ b/phpstan.tests.neon @@ -10,4 +10,5 @@ parameters: - '#Parameter \#1 \$resource of class League\\Csv\\Stream constructor expects resource, string given.#' - '#Parameter \#1 \$records of method League\\Csv\\CharsetConverter::convert\(\) expects array|Traversable, string given.#' - '#Parameter \#2 \$delimiters of function League\\Csv\\delimiter_detect expects array, array given.#' + - '#Parameter \#1 \$document of class League\\Csv\\RFC4180Iterator constructor expects League\\Csv\\Stream\|SplFileObject, array given.#' reportUnmatchedIgnoredErrors: false diff --git a/src/AbstractCsv.php b/src/AbstractCsv.php index 790a1091..e27e13cf 100644 --- a/src/AbstractCsv.php +++ b/src/AbstractCsv.php @@ -23,7 +23,6 @@ use const FILTER_SANITIZE_STRING; use function filter_var; use function get_class; -use function implode; use function mb_strlen; use function rawurlencode; use function sprintf; @@ -91,12 +90,6 @@ abstract class AbstractCsv implements ByteSequence */ protected $escape = '\\'; - /** - * Does (fput|fget)csv supports the empty string escape. - * @var bool - */ - protected static $has_native_support_for_empty_string_escape_char; - /** * The CSV document. * @@ -121,11 +114,6 @@ protected function __construct($document) */ protected function resetProperties() { - if (null === static::$has_native_support_for_empty_string_escape_char) { - //This should be replace by a polyfill targetting PHP_VERSION constant - $res = @fgetcsv(tmpfile(), 0, ',', '"', ''); - static::$has_native_support_for_empty_string_escape_char = false !== $res; - } } /** @@ -212,23 +200,6 @@ public function getEscape(): string return $this->escape; } - /** - * Computes the escape character. - * - * If the escape character is the empty string and the native fgetcsv and fputcsv - * do not supports the empty string it is replaced by the null byte character. - * - * THIS HACK may not work if the CSV content contains a null byte character. - */ - protected function getEscapeChar(): string - { - if ('' === $this->escape && !self::$has_native_support_for_empty_string_escape_char) { - return "\0"; - } - - return $this->escape; - } - /** * Returns the BOM sequence in use on Output methods. */ @@ -247,9 +218,8 @@ public function getInputBOM(): string } $this->document->setFlags(SplFileObject::READ_CSV); - $this->document->setCsvControl($this->delimiter, $this->enclosure, $this->getEscapeChar()); $this->document->rewind(); - $this->input_bom = bom_match(implode(',', (array) $this->document->current())); + $this->input_bom = bom_match((string) $this->document->fread(20)); return $this->input_bom; } diff --git a/src/RFC4180Field.php b/src/RFC4180Field.php index 77db25b1..b10efb8b 100644 --- a/src/RFC4180Field.php +++ b/src/RFC4180Field.php @@ -37,7 +37,7 @@ * DEPRECATION WARNING! This class will be removed in the next major point release * * @deprecated deprecated since version 9.2.0 - * @see Writer::insertOne + * @see AbstractCsv::setEscape * * @see https://tools.ietf.org/html/rfc4180#section-2 * diff --git a/src/RFC4180Iterator.php b/src/RFC4180Iterator.php new file mode 100644 index 00000000..497988e6 --- /dev/null +++ b/src/RFC4180Iterator.php @@ -0,0 +1,164 @@ + + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Csv; + +use IteratorAggregate; +use SplFileObject; +use TypeError; +use function get_class; +use function gettype; +use function is_object; +use function sprintf; +use function substr; + +/** + * A RFC4180 Compliant Parser in Pure PHP. + * + * @see https://php.net/manual/en/function.fgetcsv.php + * @see https://php.net/manual/en/function.fgetc.php + * @see https://tools.ietf.org/html/rfc4180 + * @see http://edoceo.com/utilitas/csv-file-format + * + * @package League.csv + * @since 9.2.0 + * @author Ignace Nyamagana Butera + * @internal used internally to produce RFC4180 compliant records + */ +final class RFC4180Iterator implements IteratorAggregate +{ + /** + * The CSV document. + * + * @var SplFileObject|Stream + */ + private $document; + + /** + * New instance. + * + * @param SplFileObject|Stream $document + */ + public function __construct($document) + { + if (!$document instanceof Stream && !$document instanceof SplFileObject) { + throw new TypeError(sprintf( + 'Expected a %s or an SplFileObject object, % given', + Stream::class, + is_object($document) ? get_class($document) : gettype($document) + )); + } + + $this->document = $document; + } + + /** + * @inheritdoc + * + * Converts the stream into a CSV record iterator + */ + public function getIterator() + { + //initialisation + $record = []; + $buffer = ''; + $previous_char = ''; + $enclosed_field = false; + list($delimiter, $enclosure, ) = $this->document->getCsvControl(); + $this->document->rewind(); + + //let's walk through the stream a char by char + while (false !== ($char = $this->document->fgetc())) { + switch ($char) { + case $enclosure: + if (!$enclosed_field) { + //the enclosure is at the start of the record + //this is an enclosed field + if ('' === $buffer) { + $enclosed_field = true; + break; + } + //invalid CSV content let's deal with it like fgetcsv + //we add the character to the buffer and we move on + $previous_char = $char; + $buffer .= $char; + break; + } + //double quoted enclosure let's skip the character and move on + if ($previous_char === $enclosure) { + $previous_char = ''; + break; + } + $previous_char = $char; + $buffer .= $char; + break; + case $delimiter: + if ($enclosed_field) { + //the delimiter is enclosed let's add it to the buffer and move on + if ($previous_char !== $enclosure) { + $buffer .= $char; + break; + } + //strip the enclosure character present at the + //end of the buffer; this is the end of en enclosed field + $buffer = substr($buffer, 0, -1); + } + + //the buffer is the field content we add it to the record + $record[] = $buffer; + + //reset field parameters + $buffer = ''; + $previous_char = ''; + $enclosed_field = false; + break; + case "\n": + case "\r": + if ($enclosed_field) { + //the line break is enclosed let's add it to the buffer and move on + if ($previous_char !== $enclosure) { + $previous_char = $char; + $buffer .= $char; + break; + } + //strip the enclosure character present at the + //end of the buffer; this is the end of a record + $buffer = substr($buffer, 0, -1); + } + + //adding field content to the record + $record[] = $buffer; + //reset field parameters + $buffer = ''; + $enclosed_field = false; + $previous_char = ''; + + //yield the record + yield $record; + + //reset record + $record = []; + break; + default: + $buffer .= $char; + break; + } + } + $record[] = $buffer; + + yield $record; + } +} diff --git a/src/Reader.php b/src/Reader.php index cd3f438c..d4c9891c 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -153,25 +153,37 @@ protected function setHeader(int $offset): array /** * Returns the row at a given offset. + * + * @return array|false */ protected function seekRow(int $offset) { - $this->document->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY); - $this->document->setCsvControl($this->delimiter, $this->enclosure, $this->getEscapeChar()); - $this->document->rewind(); - - //Workaround for SplFileObject::seek bug in PHP7.2+ see https://bugs.php.net/bug.php?id=75917 - if (PHP_VERSION_ID >= 70200 && !$this->document instanceof Stream) { - while ($offset !== $this->document->key() && $this->document->valid()) { - $this->document->next(); + $document = $this->getDocument(); + foreach ($document as $index => $record) { + if ($offset === $index) { + return $record; } + } + + return false; + } - return $this->document->current(); + /** + * Returns the document as an Iterator. + */ + protected function getDocument(): Iterator + { + if ('' === $this->escape && PHP_VERSION_ID < 70400) { + $this->document->setCsvControl($this->delimiter, $this->enclosure); + + return (new RFC4180Iterator($this->document))->getIterator(); } - $this->document->seek($offset); + $this->document->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY); + $this->document->setCsvControl($this->delimiter, $this->enclosure, $this->escape); + $this->document->rewind(); - return $this->document->current(); + return $this->document; } /** @@ -183,7 +195,7 @@ protected function seekRow(int $offset) */ protected function removeBOM(array $record, int $bom_length, string $enclosure): array { - if (0 == $bom_length) { + if (0 === $bom_length) { return $record; } @@ -259,10 +271,9 @@ public function getRecords(array $header = []): Iterator return is_array($record) && $record != [null]; }; $bom = $this->getInputBOM(); - $this->document->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY); - $this->document->setCsvControl($this->delimiter, $this->enclosure, $this->getEscapeChar()); + $document = $this->getDocument(); - $records = $this->stripBOM(new CallbackFilterIterator($this->document, $normalized), $bom); + $records = $this->stripBOM(new CallbackFilterIterator($document, $normalized), $bom); if (null !== $this->header_offset) { $records = new CallbackFilterIterator($records, function (array $record, int $offset): bool { return $offset !== $this->header_offset; diff --git a/src/Stream.php b/src/Stream.php index ad8ac6cc..c4653de6 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -26,6 +26,7 @@ use function fclose; use function feof; use function fflush; +use function fgetc; use function fgetcsv; use function fopen; use function fpassthru; @@ -449,6 +450,18 @@ public function fread($length) return fread($this->stream, $length); } + /** + * Gets character from file. + * + * @see http://php.net/manual/en/splfileobject.fgetc.php + * + * @return string|false + */ + public function fgetc() + { + return fgetc($this->stream); + } + /** * Seek to a position. * diff --git a/src/Writer.php b/src/Writer.php index e7969068..1e04ef38 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -155,7 +155,7 @@ public function insertAll($records): int public function insertOne(array $record): int { $method = 'addRecord'; - if ('' === $this->escape && !static::$has_native_support_for_empty_string_escape_char) { + if ('' === $this->escape && PHP_VERSION_ID < 70400) { $method = 'addRFC4180CompliantRecord'; } @@ -199,7 +199,7 @@ protected function addRecord(array $record) * * Fields must be delimited with enclosures if they contains : * - * - Leading or trailing whitespaces + * - Embedded whitespaces * - Embedded delimiters * - Embedded line-breaks * - Embedded enclosures. diff --git a/tests/CsvTest.php b/tests/CsvTest.php index a940dc86..3d7fd6df 100644 --- a/tests/CsvTest.php +++ b/tests/CsvTest.php @@ -18,7 +18,6 @@ use League\Csv\Reader; use League\Csv\Writer; use PHPUnit\Framework\TestCase; -use ReflectionClass; use SplTempFileObject; use const PHP_EOL; use const PHP_VERSION; @@ -423,21 +422,4 @@ public function testIsIterablePolyFill() self::assertFalse(is_iterable((object) ['foo'])); self::assertFalse(is_iterable(Writer::createFromString(''))); } - - /** - * @covers ::setDelimiter - * @covers ::resetProperties - */ - public function testResetProperties() - { - $csv = Writer::createFromString(); - $csv->setDelimiter('|'); - $ref = new ReflectionClass($csv); - $prop = $ref->getProperty('has_native_support_for_empty_string_escape_char'); - $prop->setAccessible(true); - $prop->setValue(null); - self::assertNull($prop->getValue()); - $csv->setDelimiter(';'); - self::assertFalse($prop->getValue()); - } } diff --git a/tests/RFC4180IteratorTest.php b/tests/RFC4180IteratorTest.php new file mode 100644 index 00000000..c14dbf61 --- /dev/null +++ b/tests/RFC4180IteratorTest.php @@ -0,0 +1,75 @@ + + * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) + * @version 9.1.5 + * @link https://github.com/thephpleague/csv + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace LeagueTest\Csv; + +use League\Csv\Reader; +use League\Csv\RFC4180Iterator; +use PHPUnit\Framework\TestCase; +use SplTempFileObject; +use TypeError; + +/** + * @group reader + * @coversDefaultClass League\Csv\RFC4180Iterator + */ +class RFC4180IteratorTest extends TestCase +{ + public function testConstructorThrowsTypeErrorWithUnknownDocument() + { + self::expectException(TypeError::class); + new RFC4180Iterator([]); + } + + /** + * @covers ::getIterator + * @covers \League\Csv\Stream::fgetc + * @covers \League\Csv\Reader::getDocument + */ + public function testReaderWithEmptyEscapeChar1() + { + $source = <<setEscape(''); + self::assertCount(5, $csv); + $csv->setHeaderOffset(0); + self::assertCount(4, $csv); + } + + /** + * @covers ::getIterator + * @covers \League\Csv\Reader::getDocument + */ + public function testReaderWithEmptyEscapeChar2() + { + $source = '"parent name","child name","title" + "parentA","childA","titleA"'; + + $spl = new SplTempFileObject(); + $spl->fwrite($source); + + $csv = Reader::createFromFileObject($spl); + $csv->setEscape(''); + self::assertCount(2, $csv); + $csv->setHeaderOffset(0); + self::assertCount(1, $csv); + } +} diff --git a/tests/ReaderTest.php b/tests/ReaderTest.php index f4c78b5d..247bbc25 100644 --- a/tests/ReaderTest.php +++ b/tests/ReaderTest.php @@ -71,6 +71,39 @@ public function testCountable() "parentA","childA","titleA"'; $csv = Reader::createFromString($source); self::assertCount(2, $csv); + $csv->setHeaderOffset(0); + self::assertCount(1, $csv); + } + + /** + * @covers \League\Csv\RFC4180Iterator + */ + public function testReaderWithEmptyEscapeChar1() + { + $source = <<setEscape(''); + self::assertCount(5, $csv); + $csv->setHeaderOffset(0); + self::assertCount(4, $csv); + } + + /** + * @covers \League\Csv\RFC4180Iterator + */ + public function testReaderWithEmptyEscapeChar2() + { + $source = '"parent name","child name","title" + "parentA","childA","titleA"'; + $csv = Reader::createFromString($source); + $csv->setEscape(''); self::assertCount(2, $csv); $csv->setHeaderOffset(0); self::assertCount(1, $csv); diff --git a/tests/WriterTest.php b/tests/WriterTest.php index 29a4dbed..b2440a33 100644 --- a/tests/WriterTest.php +++ b/tests/WriterTest.php @@ -254,7 +254,6 @@ public function testWriterTriggerExceptionWithNonSeekableStream() * @see https://bugs.php.net/bug.php?id=55413 * * @covers ::getInputBOM - * @covers ::getEscapeChar * @covers ::insertOne * @covers ::addRFC4180CompliantRecord * From 8d8d7da6670e161428d5a15f4c92e164448ceb68 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Fri, 14 Sep 2018 14:33:40 +0200 Subject: [PATCH 047/858] Improve RFC4180 parser --- src/RFC4180Iterator.php | 20 +++++++++++++------- src/XMLConverter.php | 2 +- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/RFC4180Iterator.php b/src/RFC4180Iterator.php index 497988e6..7c386c64 100644 --- a/src/RFC4180Iterator.php +++ b/src/RFC4180Iterator.php @@ -74,20 +74,20 @@ public function getIterator() { //initialisation $record = []; - $buffer = ''; + $buffer = null; $previous_char = ''; $enclosed_field = false; list($delimiter, $enclosure, ) = $this->document->getCsvControl(); $this->document->rewind(); - //let's walk through the stream a char by char + //let's walk through the stream char by char while (false !== ($char = $this->document->fgetc())) { switch ($char) { case $enclosure: if (!$enclosed_field) { //the enclosure is at the start of the record - //this is an enclosed field - if ('' === $buffer) { + //so we have an enclosed field + if (null === $buffer) { $enclosed_field = true; break; } @@ -99,6 +99,8 @@ public function getIterator() } //double quoted enclosure let's skip the character and move on if ($previous_char === $enclosure) { + //we reset the previous character + //to avoid stripping to much enclosure character $previous_char = ''; break; } @@ -110,6 +112,7 @@ public function getIterator() //the delimiter is enclosed let's add it to the buffer and move on if ($previous_char !== $enclosure) { $buffer .= $char; + $previous_char = $char; break; } //strip the enclosure character present at the @@ -118,10 +121,11 @@ public function getIterator() } //the buffer is the field content we add it to the record - $record[] = $buffer; + //and convert it into a string + $record[] = ''.$buffer; //reset field parameters - $buffer = ''; + $buffer = null; $previous_char = ''; $enclosed_field = false; break; @@ -142,7 +146,7 @@ public function getIterator() //adding field content to the record $record[] = $buffer; //reset field parameters - $buffer = ''; + $buffer = null; $enclosed_field = false; $previous_char = ''; @@ -153,12 +157,14 @@ public function getIterator() $record = []; break; default: + $previous_char = $char; $buffer .= $char; break; } } $record[] = $buffer; + //yield remaining record yield $record; } } diff --git a/src/XMLConverter.php b/src/XMLConverter.php index c6662c1f..46fb09ac 100644 --- a/src/XMLConverter.php +++ b/src/XMLConverter.php @@ -135,7 +135,7 @@ protected function recordToElement(DOMDocument $doc, array $record, string $fiel { $node = $doc->createElement($this->record_name); foreach ($record as $node_name => $value) { - $item = $this->$field_encoder($doc, $value, $node_name); + $item = $this->$field_encoder($doc, (string) $value, $node_name); $node->appendChild($item); } From 8ffbd5d6a05d9c432193dcfbc916af1da475a9ed Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Sat, 15 Sep 2018 08:44:01 +0200 Subject: [PATCH 048/858] Improve RFC4180 test suite --- src/RFC4180Iterator.php | 27 +++++++++++- tests/RFC4180IteratorTest.php | 79 ++++++++++++++++++++++++++--------- tests/ReaderTest.php | 4 +- 3 files changed, 87 insertions(+), 23 deletions(-) diff --git a/src/RFC4180Iterator.php b/src/RFC4180Iterator.php index 7c386c64..74cc4daf 100644 --- a/src/RFC4180Iterator.php +++ b/src/RFC4180Iterator.php @@ -24,6 +24,7 @@ use function is_object; use function sprintf; use function substr; +use function trim; /** * A RFC4180 Compliant Parser in Pure PHP. @@ -78,6 +79,7 @@ public function getIterator() $previous_char = ''; $enclosed_field = false; list($delimiter, $enclosure, ) = $this->document->getCsvControl(); + $trim_mask = str_replace([$delimiter, $enclosure], '', " \t\0\x0B"); $this->document->rewind(); //let's walk through the stream char by char @@ -122,7 +124,12 @@ public function getIterator() //the buffer is the field content we add it to the record //and convert it into a string - $record[] = ''.$buffer; + $buffer = ''.$buffer; + //if the field is not enclose we trim white spaces + if (!$enclosed_field) { + $buffer = trim($buffer, $trim_mask); + } + $record[] = $buffer; //reset field parameters $buffer = null; @@ -143,6 +150,10 @@ public function getIterator() $buffer = substr($buffer, 0, -1); } + //if the field is not enclose we trim white spaces + if (null !== $buffer && !$enclosed_field) { + $buffer = trim($buffer, $trim_mask); + } //adding field content to the record $record[] = $buffer; //reset field parameters @@ -162,9 +173,21 @@ public function getIterator() break; } } + + //yield the remaining buffer + if ($enclosed_field && $enclosure === substr($buffer, -1, 1)) { + //strip the enclosure character present at the + //end of the buffer; this is the end of en enclosed field + $buffer = substr($buffer, 0, -1); + } + + //if the field is not enclose we trim white spaces + if (!$enclosed_field && null !== $buffer) { + $buffer = trim($buffer, $trim_mask); + } + $record[] = $buffer; - //yield remaining record yield $record; } } diff --git a/tests/RFC4180IteratorTest.php b/tests/RFC4180IteratorTest.php index c14dbf61..ec5c777f 100644 --- a/tests/RFC4180IteratorTest.php +++ b/tests/RFC4180IteratorTest.php @@ -14,8 +14,8 @@ namespace LeagueTest\Csv; -use League\Csv\Reader; use League\Csv\RFC4180Iterator; +use League\Csv\Stream; use PHPUnit\Framework\TestCase; use SplTempFileObject; use TypeError; @@ -26,6 +26,9 @@ */ class RFC4180IteratorTest extends TestCase { + /** + * @covers ::__construct + */ public function testConstructorThrowsTypeErrorWithUnknownDocument() { self::expectException(TypeError::class); @@ -33,11 +36,11 @@ public function testConstructorThrowsTypeErrorWithUnknownDocument() } /** + * @covers ::__construct * @covers ::getIterator * @covers \League\Csv\Stream::fgetc - * @covers \League\Csv\Reader::getDocument */ - public function testReaderWithEmptyEscapeChar1() + public function testWorksWithMultiLines() { $source = <<setEscape(''); - self::assertCount(5, $csv); - $csv->setHeaderOffset(0); - self::assertCount(4, $csv); + + $multiline = <<getIterator(), false); + self::assertSame($multiline, $data[4][3]); } /** * @covers ::getIterator - * @covers \League\Csv\Reader::getDocument */ - public function testReaderWithEmptyEscapeChar2() + public function testKeepEmptyLines() { - $source = '"parent name","child name","title" - "parentA","childA","titleA"'; + $source = <<fwrite($source); + $iterator = new RFC4180Iterator($rsrc); + + self::assertCount(4, $iterator); + $data = iterator_to_array($iterator->getIterator(), false); + self::assertSame(['parent name', 'child name', 'title'], $data[0]); + self::assertSame([0 => null], $data[1]); + self::assertSame([0 => null], $data[2]); + self::assertSame(['parentA', 'childA', 'titleA'], $data[3]); + } - $spl = new SplTempFileObject(); - $spl->fwrite($source); + /** + * @covers ::getIterator + */ + public function testTrimSpaceWithNotEncloseField() + { + $source = <<getIterator(), false); + self::assertSame(['Year', 'Make', 'Model', '', 'Description', 'Price'], $data[0]); + self::assertSame(['1997', 'Ford', 'E350', 'ac, abs, moon', '3000.00'], $data[1]); + } - $csv = Reader::createFromFileObject($spl); - $csv->setEscape(''); - self::assertCount(2, $csv); - $csv->setHeaderOffset(0); - self::assertCount(1, $csv); + public function testHandlingInvalidCSV() + { + $source = <<getIterator(), false); + self::assertSame(['Year"', 'Make', 'Model', 'Description', 'Price'], $data[0]); + self::assertSame(['1997', 'Ford', 'E350', 'ac, abs, moon', '3000.00'], $data[1]); } } diff --git a/tests/ReaderTest.php b/tests/ReaderTest.php index 247bbc25..b4f491fc 100644 --- a/tests/ReaderTest.php +++ b/tests/ReaderTest.php @@ -76,7 +76,7 @@ public function testCountable() } /** - * @covers \League\Csv\RFC4180Iterator + * @covers ::getDocument */ public function testReaderWithEmptyEscapeChar1() { @@ -96,7 +96,7 @@ public function testReaderWithEmptyEscapeChar1() } /** - * @covers \League\Csv\RFC4180Iterator + * @covers ::getDocument */ public function testReaderWithEmptyEscapeChar2() { From f8ec52328abe07720be73fe37838b9c5e102afb3 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Sun, 16 Sep 2018 15:52:06 +0200 Subject: [PATCH 049/858] Improve RFC4180Iterator --- src/RFC4180Iterator.php | 253 +++++++++++++++++++++------------- src/Stream.php | 10 +- tests/RFC4180IteratorTest.php | 74 +++++++--- 3 files changed, 214 insertions(+), 123 deletions(-) diff --git a/src/RFC4180Iterator.php b/src/RFC4180Iterator.php index 74cc4daf..6b6e3d03 100644 --- a/src/RFC4180Iterator.php +++ b/src/RFC4180Iterator.php @@ -23,6 +23,7 @@ use function gettype; use function is_object; use function sprintf; +use function str_split; use function substr; use function trim; @@ -30,7 +31,7 @@ * A RFC4180 Compliant Parser in Pure PHP. * * @see https://php.net/manual/en/function.fgetcsv.php - * @see https://php.net/manual/en/function.fgetc.php + * @see https://php.net/manual/en/function.fgets.php * @see https://tools.ietf.org/html/rfc4180 * @see http://edoceo.com/utilitas/csv-file-format * @@ -48,6 +49,35 @@ final class RFC4180Iterator implements IteratorAggregate */ private $document; + /** + * @var string + */ + private $delimiter; + /** + * @var string + */ + private $enclosure; + + /** + * @var string|null + */ + private $buffer; + + /** + * @var string + */ + private $previous_char = ''; + + /** + * @var bool + */ + private $enclosed_field = false; + + /** + * @var string + */ + private $trim_mask; + /** * New instance. * @@ -74,120 +104,145 @@ public function __construct($document) public function getIterator() { //initialisation - $record = []; - $buffer = null; - $previous_char = ''; - $enclosed_field = false; - list($delimiter, $enclosure, ) = $this->document->getCsvControl(); - $trim_mask = str_replace([$delimiter, $enclosure], '', " \t\0\x0B"); + list($this->delimiter, $this->enclosure, ) = $this->document->getCsvControl(); + $this->trim_mask = str_replace([$this->delimiter, $this->enclosure], '', " \t\0\x0B"); + $this->document->setFlags(0); $this->document->rewind(); + $this->flush(); - //let's walk through the stream char by char - while (false !== ($char = $this->document->fgetc())) { - switch ($char) { - case $enclosure: - if (!$enclosed_field) { - //the enclosure is at the start of the record - //so we have an enclosed field - if (null === $buffer) { - $enclosed_field = true; - break; - } - //invalid CSV content let's deal with it like fgetcsv - //we add the character to the buffer and we move on - $previous_char = $char; - $buffer .= $char; - break; - } - //double quoted enclosure let's skip the character and move on - if ($previous_char === $enclosure) { - //we reset the previous character - //to avoid stripping to much enclosure character - $previous_char = ''; - break; - } - $previous_char = $char; - $buffer .= $char; - break; - case $delimiter: - if ($enclosed_field) { - //the delimiter is enclosed let's add it to the buffer and move on - if ($previous_char !== $enclosure) { - $buffer .= $char; - $previous_char = $char; - break; - } - //strip the enclosure character present at the - //end of the buffer; this is the end of en enclosed field - $buffer = substr($buffer, 0, -1); - } - - //the buffer is the field content we add it to the record - //and convert it into a string - $buffer = ''.$buffer; - //if the field is not enclose we trim white spaces - if (!$enclosed_field) { - $buffer = trim($buffer, $trim_mask); - } - $record[] = $buffer; - - //reset field parameters - $buffer = null; - $previous_char = ''; - $enclosed_field = false; - break; - case "\n": - case "\r": - if ($enclosed_field) { - //the line break is enclosed let's add it to the buffer and move on - if ($previous_char !== $enclosure) { - $previous_char = $char; - $buffer .= $char; - break; - } - //strip the enclosure character present at the - //end of the buffer; this is the end of a record - $buffer = substr($buffer, 0, -1); - } - - //if the field is not enclose we trim white spaces - if (null !== $buffer && !$enclosed_field) { - $buffer = trim($buffer, $trim_mask); - } - //adding field content to the record - $record[] = $buffer; - //reset field parameters - $buffer = null; - $enclosed_field = false; - $previous_char = ''; - - //yield the record + $methodList = [ + $this->enclosure => 'processEnclosure', + $this->delimiter => 'processBreaks', + "\n" => 'processBreaks', + "\r" => 'processBreaks', + ]; + + $record = []; + while ($this->document->valid()) { + //let's walk through the stream char by char + foreach (str_split((string) $this->document->fgets()) as $char) { + $method = $methodList[$char] ?? 'addCharacter'; + if ('processBreaks' !== $method) { + $this->$method($char); + continue; + } + + $field = $this->$method($char); + if (null !== $this->buffer) { + continue; + } + + $record[] = $field; + if ($char !== $this->delimiter) { yield $record; - //reset record $record = []; - break; - default: - $previous_char = $char; - $buffer .= $char; - break; + } } } + $record[] = $this->clean(); + + yield $record; + } + + private function clean() + { //yield the remaining buffer - if ($enclosed_field && $enclosure === substr($buffer, -1, 1)) { + if ($this->enclosed_field && $this->enclosure === $this->previous_char) { //strip the enclosure character present at the //end of the buffer; this is the end of en enclosed field - $buffer = substr($buffer, 0, -1); + $this->buffer = substr($this->buffer, 0, -1); } + return $this->flush(); + } + + /** + * Format and return the field content. + * + * @return string|null + */ + private function flush() + { //if the field is not enclose we trim white spaces - if (!$enclosed_field && null !== $buffer) { - $buffer = trim($buffer, $trim_mask); + if (null !== $this->buffer && !$this->enclosed_field) { + $this->buffer = trim($this->buffer, $this->trim_mask); } - $record[] = $buffer; + //adding field content to the record + $field = $this->buffer; - yield $record; + //reset parameters + $this->buffer = null; + $this->previous_char = ''; + $this->enclosed_field = false; + + return $field; + } + + /** + * Append a character to the buffer. + * + */ + private function addCharacter(string $char) + { + $this->previous_char = $char; + $this->buffer .= $char; + } + + /** + * Handle enclosure presence. + */ + private function processEnclosure(string $char) + { + if (!$this->enclosed_field) { + //the enclosure is at the start of the record + //so we have an enclosed field + if (null === $this->buffer) { + $this->enclosed_field = true; + return; + } + //invalid CSV content let's deal with it like fgetcsv + //we add the character to the buffer and we move on + return $this->addCharacter($char); + } + + //double enclosure let's skip the character and move on + if ($this->previous_char === $char) { + //we reset the previous character to the empty string + //to only strip double enclosure characters + $this->previous_char = ''; + return; + } + + return $this->addCharacter($char); + } + + /** + * Handle delimiter and line breaks. + * + * @return null|string + */ + private function processBreaks(string $char) + { + if ($char === $this->delimiter) { + $this->buffer = (string) $this->buffer; + } + + if (!$this->enclosed_field) { + return $this->flush(); + } + + //the line break is enclosed let's add it to the buffer and move on + if ($this->previous_char !== $this->enclosure) { + return $this->addCharacter($char); + } + + //strip the enclosure character present at the + //end of the buffer; this is the end of a record + $this->buffer = substr($this->buffer, 0, -1); + + return $this->flush(); } } diff --git a/src/Stream.php b/src/Stream.php index c4653de6..b4a3498c 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -26,8 +26,8 @@ use function fclose; use function feof; use function fflush; -use function fgetc; use function fgetcsv; +use function fgets; use function fopen; use function fpassthru; use function fputcsv; @@ -451,15 +451,15 @@ public function fread($length) } /** - * Gets character from file. + * Gets a line from file. * - * @see http://php.net/manual/en/splfileobject.fgetc.php + * @see http://php.net/manual/en/splfileobject.fgets.php * * @return string|false */ - public function fgetc() + public function fgets() { - return fgetc($this->stream); + return fgets($this->stream); } /** diff --git a/tests/RFC4180IteratorTest.php b/tests/RFC4180IteratorTest.php index ec5c777f..9cad32c2 100644 --- a/tests/RFC4180IteratorTest.php +++ b/tests/RFC4180IteratorTest.php @@ -26,9 +26,6 @@ */ class RFC4180IteratorTest extends TestCase { - /** - * @covers ::__construct - */ public function testConstructorThrowsTypeErrorWithUnknownDocument() { self::expectException(TypeError::class); @@ -36,16 +33,20 @@ public function testConstructorThrowsTypeErrorWithUnknownDocument() } /** - * @covers ::__construct + * @covers \League\Csv\Stream::fgets * @covers ::getIterator - * @covers \League\Csv\Stream::fgetc + * @covers ::processBreaks + * @covers ::processEnclosure + * @covers ::addCharacter + * @covers ::flush + * @covers ::clean */ public function testWorksWithMultiLines() { $source = <<setCsvControl('|', "'"); + $iterator = new RFC4180Iterator($doc); + self::assertCount(5, $iterator); + $data = iterator_to_array($iterator->getIterator(), false); + self::assertSame($multiline, $data[4][3]); + } + public function testKeepEmptyLines() { $source = <<getIterator(), false); - self::assertSame(['Year"', 'Make', 'Model', 'Description', 'Price'], $data[0]); - self::assertSame(['1997', 'Ford', 'E350', 'ac, abs, moon', '3000.00'], $data[1]); + self::assertSame($record, $data[0]); + } + + public function invalidCsvRecordProvider() + { + return [ + 'enclosure inside a non-unclosed field' => [ + 'string' => 'Ye"ar,Make",Model,Description,Price', + 'record' => ['Ye"ar', 'Make"', 'Model', 'Description', 'Price'], + ], + 'enclosure at the end of a non-unclosed field' => [ + 'string' => 'Year,Make,Model,Description,Price"', + 'record' => ['Year', 'Make', 'Model', 'Description', 'Price"'], + ], + 'enclosure at the end of a record field' => [ + 'string' => 'Year,Make,Model,Description,"Price', + 'record' => ['Year', 'Make', 'Model', 'Description', 'Price'], + ], + ]; } } From d65b3f5702bc8811a5c8f21de31066cb6064d7f4 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Sun, 16 Sep 2018 21:41:11 +0200 Subject: [PATCH 050/858] improve code --- src/RFC4180Iterator.php | 105 +++++++++++++++++----------------- tests/RFC4180IteratorTest.php | 9 ++- 2 files changed, 58 insertions(+), 56 deletions(-) diff --git a/src/RFC4180Iterator.php b/src/RFC4180Iterator.php index 6b6e3d03..93bf9a8d 100644 --- a/src/RFC4180Iterator.php +++ b/src/RFC4180Iterator.php @@ -43,8 +43,6 @@ final class RFC4180Iterator implements IteratorAggregate { /** - * The CSV document. - * * @var SplFileObject|Stream */ private $document; @@ -66,12 +64,12 @@ final class RFC4180Iterator implements IteratorAggregate /** * @var string */ - private $previous_char = ''; + private $previous_char; /** * @var bool */ - private $enclosed_field = false; + private $enclosed_field; /** * @var string @@ -104,48 +102,47 @@ public function __construct($document) public function getIterator() { //initialisation + $this->init(); list($this->delimiter, $this->enclosure, ) = $this->document->getCsvControl(); $this->trim_mask = str_replace([$this->delimiter, $this->enclosure], '', " \t\0\x0B"); $this->document->setFlags(0); $this->document->rewind(); - $this->flush(); - - $methodList = [ - $this->enclosure => 'processEnclosure', - $this->delimiter => 'processBreaks', - "\n" => 'processBreaks', - "\r" => 'processBreaks', - ]; $record = []; - while ($this->document->valid()) { - //let's walk through the stream char by char - foreach (str_split((string) $this->document->fgets()) as $char) { - $method = $methodList[$char] ?? 'addCharacter'; - if ('processBreaks' !== $method) { - $this->$method($char); + do { + $line = (string) $this->document->fgets(); + foreach (str_split($line) as $char) { + if (!in_array($char, [$this->delimiter, "\n", "\r"], true)) { + $this->processEnclosure($char); continue; } - $field = $this->$method($char); + $field = $this->processBreaks($char); if (null !== $this->buffer) { continue; } $record[] = $field; - if ($char !== $this->delimiter) { - yield $record; - - $record = []; + if ($char === $this->delimiter) { + continue; } + + yield $record; + + $record = []; } - } + } while ($this->document->valid()); $record[] = $this->clean(); yield $record; } + /** + * Flushes and returns the last field content. + * + * @return string|null + */ private function clean() { //yield the remaining buffer @@ -159,68 +156,72 @@ private function clean() } /** - * Format and return the field content. + * Flushes and returns the field content. + * + * If the field is not enclose we trim white spaces cf RFC4180 * * @return string|null */ private function flush() { - //if the field is not enclose we trim white spaces if (null !== $this->buffer && !$this->enclosed_field) { $this->buffer = trim($this->buffer, $this->trim_mask); } - //adding field content to the record $field = $this->buffer; - - //reset parameters - $this->buffer = null; - $this->previous_char = ''; - $this->enclosed_field = false; + $this->init(); return $field; } /** - * Append a character to the buffer. - * + * Initialize the internal properties. */ - private function addCharacter(string $char) + private function init() { - $this->previous_char = $char; - $this->buffer .= $char; + $this->buffer = null; + $this->previous_char = ''; + $this->enclosed_field = false; } /** - * Handle enclosure presence. + * Handles enclosure presence according to RFC4180. + * + * - detect enclosed field + * - convert the double enclosure to one enclosure */ private function processEnclosure(string $char) { + if ($char !== $this->enclosure) { + $this->previous_char = $char; + $this->buffer .= $char; + return; + } + if (!$this->enclosed_field) { - //the enclosure is at the start of the record - //so we have an enclosed field if (null === $this->buffer) { $this->enclosed_field = true; return; } - //invalid CSV content let's deal with it like fgetcsv - //we add the character to the buffer and we move on - return $this->addCharacter($char); + //invalid CSV content + $this->previous_char = $char; + $this->buffer .= $char; + return; } - //double enclosure let's skip the character and move on + //double enclosure if ($this->previous_char === $char) { - //we reset the previous character to the empty string - //to only strip double enclosure characters + //safe check to only strip double enclosure characters $this->previous_char = ''; return; } - return $this->addCharacter($char); + $this->previous_char = $char; + $this->buffer .= $char; } /** - * Handle delimiter and line breaks. + * Handles delimiter and line breaks according to RFC4180. * * @return null|string */ @@ -234,13 +235,15 @@ private function processBreaks(string $char) return $this->flush(); } - //the line break is enclosed let's add it to the buffer and move on + //the delimiter or the line break is enclosed if ($this->previous_char !== $this->enclosure) { - return $this->addCharacter($char); + $this->previous_char = $char; + $this->buffer .= $char; + return null; } //strip the enclosure character present at the - //end of the buffer; this is the end of a record + //end of the buffer; this is the end of a field $this->buffer = substr($this->buffer, 0, -1); return $this->flush(); diff --git a/tests/RFC4180IteratorTest.php b/tests/RFC4180IteratorTest.php index 9cad32c2..d46a51e3 100644 --- a/tests/RFC4180IteratorTest.php +++ b/tests/RFC4180IteratorTest.php @@ -37,7 +37,6 @@ public function testConstructorThrowsTypeErrorWithUnknownDocument() * @covers ::getIterator * @covers ::processBreaks * @covers ::processEnclosure - * @covers ::addCharacter * @covers ::flush * @covers ::clean */ @@ -73,12 +72,12 @@ public function testWorksWithMultiLinesWithDifferentDelimiter() 1999|Chevy|'Venture ''Extended Edition'''|''|4900.00 1999|Chevy|'Venture ''Extended Edition| Very Large'''||5000.00 1996|Jeep|Grand Cherokee|'MUST SELL! -air, moon roof, loaded'|4799.00 +air| moon roof| loaded'|4799.00 EOF; $multiline = <<setCsvControl('|', "'"); @@ -113,13 +112,13 @@ public function testTrimSpaceWithNotEncloseField() { $source = <<getIterator(), false); self::assertSame(['Year', 'Make', 'Model', '', 'Description', 'Price'], $data[0]); - self::assertSame(['1997', 'Ford', 'E350', 'ac, abs, moon', '3000.00'], $data[1]); + self::assertSame(['"1997', 'Ford', 'E350', 'ac, abs, moon', '3000.00'], $data[1]); } /** From 0599c14c82eccc65245180ddf3d6e1055c75ab9d Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Wed, 19 Sep 2018 13:18:21 +0200 Subject: [PATCH 051/858] Rewrite the RFC4180Iterator to use fgets --- src/RFC4180Iterator.php | 183 ++++++++++++---------------------- tests/RFC4180IteratorTest.php | 26 ++++- 2 files changed, 84 insertions(+), 125 deletions(-) diff --git a/src/RFC4180Iterator.php b/src/RFC4180Iterator.php index 93bf9a8d..da487754 100644 --- a/src/RFC4180Iterator.php +++ b/src/RFC4180Iterator.php @@ -19,11 +19,13 @@ use IteratorAggregate; use SplFileObject; use TypeError; +use function explode; use function get_class; use function gettype; use function is_object; +use function rtrim; use function sprintf; -use function str_split; +use function str_replace; use function substr; use function trim; @@ -56,21 +58,6 @@ final class RFC4180Iterator implements IteratorAggregate */ private $enclosure; - /** - * @var string|null - */ - private $buffer; - - /** - * @var string - */ - private $previous_char; - - /** - * @var bool - */ - private $enclosed_field; - /** * @var string */ @@ -102,150 +89,104 @@ public function __construct($document) public function getIterator() { //initialisation - $this->init(); list($this->delimiter, $this->enclosure, ) = $this->document->getCsvControl(); $this->trim_mask = str_replace([$this->delimiter, $this->enclosure], '', " \t\0\x0B"); $this->document->setFlags(0); $this->document->rewind(); - - $record = []; do { - $line = (string) $this->document->fgets(); - foreach (str_split($line) as $char) { - if (!in_array($char, [$this->delimiter, "\n", "\r"], true)) { - $this->processEnclosure($char); - continue; - } - - $field = $this->processBreaks($char); - if (null !== $this->buffer) { - continue; - } - - $record[] = $field; - if ($char === $this->delimiter) { - continue; - } - - yield $record; - - $record = []; - } + $line = $this->document->fgets(); + yield $this->extractRecord($line); } while ($this->document->valid()); - - $record[] = $this->clean(); - - yield $record; } /** - * Flushes and returns the last field content. + * Extract a record from the Stream document. * - * @return string|null + * @param string|bool $line */ - private function clean() + private function extractRecord($line): array { - //yield the remaining buffer - if ($this->enclosed_field && $this->enclosure === $this->previous_char) { - //strip the enclosure character present at the - //end of the buffer; this is the end of en enclosed field - $this->buffer = substr($this->buffer, 0, -1); - } + $record = []; + do { + $method = ($line[0] ?? '') === $this->enclosure ? 'extractEnclosedField' : 'extractField'; + $record[] = $this->$method($line); + } while (false !== $line); - return $this->flush(); + return $record; } /** - * Flushes and returns the field content. + * Extract field without enclosure. * - * If the field is not enclose we trim white spaces cf RFC4180 + * @param bool|string $line * - * @return string|null - */ - private function flush() - { - if (null !== $this->buffer && !$this->enclosed_field) { - $this->buffer = trim($this->buffer, $this->trim_mask); - } - - $field = $this->buffer; - $this->init(); - - return $field; - } - - /** - * Initialize the internal properties. + * @return null|string */ - private function init() + private function extractField(& $line) { - $this->buffer = null; - $this->previous_char = ''; - $this->enclosed_field = false; - } + //process the line if it is only a line-break or the empty string + if ($line === false || $line === "\r" || $line === "\r\n" || $line === "\n" || $line === '') { + $line = false; - /** - * Handles enclosure presence according to RFC4180. - * - * - detect enclosed field - * - convert the double enclosure to one enclosure - */ - private function processEnclosure(string $char) - { - if ($char !== $this->enclosure) { - $this->previous_char = $char; - $this->buffer .= $char; - return; + return null; } - if (!$this->enclosed_field) { - if (null === $this->buffer) { - $this->enclosed_field = true; - return; - } - //invalid CSV content - $this->previous_char = $char; - $this->buffer .= $char; - return; - } + //explode the line on the next delimiter character + list($content, $line) = explode($this->delimiter, $line, 2) + [1 => false]; - //double enclosure - if ($this->previous_char === $char) { - //safe check to only strip double enclosure characters - $this->previous_char = ''; - return; + //if this is the end of line remove line breaks + if (false === $line) { + $content = rtrim($content, "\r\n"); } - $this->previous_char = $char; - $this->buffer .= $char; + //remove whitespaces + return trim($content, $this->trim_mask); } /** - * Handles delimiter and line breaks according to RFC4180. + * Extract field with enclosure. + * + * @param bool|string $line * * @return null|string */ - private function processBreaks(string $char) + private function extractEnclosedField(& $line) { - if ($char === $this->delimiter) { - $this->buffer = (string) $this->buffer; + //remove the first enclosure from the line if present to easily use explode + if ($line[0] ?? '' === $this->enclosure) { + $line = substr($line, 1); } - if (!$this->enclosed_field) { - return $this->flush(); + //covers multiline fields + $content = ''; + do { + //explode the line on the next enclosure character found + list($buffer, $line) = explode($this->enclosure, $line, 2) + [1 => false]; + $content .= $buffer; + } while (false === $line && $this->document->valid() && false !== ($line = $this->document->fgets())); + + //format the field content by removing double quoting if present + $content = str_replace($this->enclosure.$this->enclosure, $this->enclosure, $content); + + //process the line if it is only a line-break or the empty string + if ($line === false || $line === "\r" || $line === "\r\n" || $line === "\n" || $line === '') { + $line = false; + + return rtrim($content, "\r\n"); } - //the delimiter or the line break is enclosed - if ($this->previous_char !== $this->enclosure) { - $this->previous_char = $char; - $this->buffer .= $char; - return null; + //the field data is extracted since we have a delimiter + if (($line[0] ?? '') === $this->delimiter) { + $line = substr($line, 1); + + return $content; } - //strip the enclosure character present at the - //end of the buffer; this is the end of a field - $this->buffer = substr($this->buffer, 0, -1); + //double quote content found + if (($line[0] ?? '') === $this->enclosure) { + $content .= '"'.$this->extractEnclosedField($line); + } - return $this->flush(); + return $content; } } diff --git a/tests/RFC4180IteratorTest.php b/tests/RFC4180IteratorTest.php index d46a51e3..c0820a5f 100644 --- a/tests/RFC4180IteratorTest.php +++ b/tests/RFC4180IteratorTest.php @@ -35,10 +35,6 @@ public function testConstructorThrowsTypeErrorWithUnknownDocument() /** * @covers \League\Csv\Stream::fgets * @covers ::getIterator - * @covers ::processBreaks - * @covers ::processEnclosure - * @covers ::flush - * @covers ::clean */ public function testWorksWithMultiLines() { @@ -148,4 +144,26 @@ public function invalidCsvRecordProvider() ], ]; } + + public function testDoubleEnclosure() + { + $str = <<setCsvControl(';'); + $records = new RFC4180Iterator($stream); + self::assertEquals($expected, iterator_to_array($records->getIterator(), false)); + } } From 4668c6b2c048fc97bf6ededf5ec20082305709bf Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Wed, 19 Sep 2018 14:09:06 +0200 Subject: [PATCH 052/858] improve RFC4180Iterator fgetcsv compliance --- src/RFC4180Iterator.php | 56 +++++++++++++++++++++-------------- tests/RFC4180IteratorTest.php | 4 +++ 2 files changed, 37 insertions(+), 23 deletions(-) diff --git a/src/RFC4180Iterator.php b/src/RFC4180Iterator.php index da487754..e71bbfc0 100644 --- a/src/RFC4180Iterator.php +++ b/src/RFC4180Iterator.php @@ -22,6 +22,7 @@ use function explode; use function get_class; use function gettype; +use function in_array; use function is_object; use function rtrim; use function sprintf; @@ -44,6 +45,11 @@ */ final class RFC4180Iterator implements IteratorAggregate { + /** + * @internal + */ + const FIELD_BREAKS = [false, "\r", "\r\n", "\n", '']; + /** * @var SplFileObject|Stream */ @@ -53,11 +59,17 @@ final class RFC4180Iterator implements IteratorAggregate * @var string */ private $delimiter; + /** * @var string */ private $enclosure; + /** + * @var string + */ + private $double_enclosure; + /** * @var string */ @@ -90,12 +102,12 @@ public function getIterator() { //initialisation list($this->delimiter, $this->enclosure, ) = $this->document->getCsvControl(); + $this->double_enclosure = $this->enclosure.$this->enclosure; $this->trim_mask = str_replace([$this->delimiter, $this->enclosure], '', " \t\0\x0B"); $this->document->setFlags(0); $this->document->rewind(); do { - $line = $this->document->fgets(); - yield $this->extractRecord($line); + yield $this->extractRecord($this->document->fgets()); } while ($this->document->valid()); } @@ -108,7 +120,10 @@ private function extractRecord($line): array { $record = []; do { - $method = ($line[0] ?? '') === $this->enclosure ? 'extractEnclosedField' : 'extractField'; + $method = 'extractField'; + if (($line[0] ?? '') === $this->enclosure) { + $method = 'extractFieldEnclosed'; + } $record[] = $this->$method($line); } while (false !== $line); @@ -122,24 +137,23 @@ private function extractRecord($line): array * * @return null|string */ - private function extractField(& $line) + private function extractField(&$line) { - //process the line if it is only a line-break or the empty string - if ($line === false || $line === "\r" || $line === "\r\n" || $line === "\n" || $line === '') { + if (in_array($line, self::FIELD_BREAKS, true)) { $line = false; return null; } - //explode the line on the next delimiter character + //explode the line on the next delimiter character if any list($content, $line) = explode($this->delimiter, $line, 2) + [1 => false]; - //if this is the end of line remove line breaks + //remove line breaks characters as per RFC4180 if (false === $line) { $content = rtrim($content, "\r\n"); } - //remove whitespaces + //remove whitespaces as per RFC4180 return trim($content, $this->trim_mask); } @@ -150,26 +164,26 @@ private function extractField(& $line) * * @return null|string */ - private function extractEnclosedField(& $line) + private function extractFieldEnclosed(&$line) { - //remove the first enclosure from the line if present to easily use explode + //remove the starting enclosure char to ease explode usage if ($line[0] ?? '' === $this->enclosure) { $line = substr($line, 1); } - //covers multiline fields $content = ''; + //cover multiline field do { - //explode the line on the next enclosure character found + //explode the line on the next enclosure character if any list($buffer, $line) = explode($this->enclosure, $line, 2) + [1 => false]; $content .= $buffer; } while (false === $line && $this->document->valid() && false !== ($line = $this->document->fgets())); - //format the field content by removing double quoting if present - $content = str_replace($this->enclosure.$this->enclosure, $this->enclosure, $content); + //decode the field content as per RFC4180 + $content = str_replace($this->double_enclosure, $this->enclosure, $content); - //process the line if it is only a line-break or the empty string - if ($line === false || $line === "\r" || $line === "\r\n" || $line === "\n" || $line === '') { + //remove line breaks characters as per RFC4180 + if (in_array($line, self::FIELD_BREAKS, true)) { $line = false; return rtrim($content, "\r\n"); @@ -182,11 +196,7 @@ private function extractEnclosedField(& $line) return $content; } - //double quote content found - if (($line[0] ?? '') === $this->enclosure) { - $content .= '"'.$this->extractEnclosedField($line); - } - - return $content; + //handles enclosure as per RFC4180 or malformed CSV like fgetcsv + return $content.($line[0] ?? '').$this->extractFieldEnclosed($line); } } diff --git a/tests/RFC4180IteratorTest.php b/tests/RFC4180IteratorTest.php index c0820a5f..5f21a5af 100644 --- a/tests/RFC4180IteratorTest.php +++ b/tests/RFC4180IteratorTest.php @@ -142,6 +142,10 @@ public function invalidCsvRecordProvider() 'string' => 'Year,Make,Model,Description,"Price', 'record' => ['Year', 'Make', 'Model', 'Description', 'Price'], ], + 'enclosure started but not ended' => [ + 'string' => 'Year,Make,Model,Description,"Pri"ce', + 'record' => ['Year', 'Make', 'Model', 'Description', 'Price'], + ], ]; } From a5f125f9c0ef03e0c7574a944cd323bff69241d1 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Wed, 19 Sep 2018 15:08:42 +0200 Subject: [PATCH 053/858] improve PR code --- src/AbstractCsv.php | 2 +- src/Writer.php | 2 ++ tests/RFC4180IteratorTest.php | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/AbstractCsv.php b/src/AbstractCsv.php index e27e13cf..1d4a0df3 100644 --- a/src/AbstractCsv.php +++ b/src/AbstractCsv.php @@ -219,7 +219,7 @@ public function getInputBOM(): string $this->document->setFlags(SplFileObject::READ_CSV); $this->document->rewind(); - $this->input_bom = bom_match((string) $this->document->fread(20)); + $this->input_bom = bom_match((string) $this->document->fread(4)); return $this->input_bom; } diff --git a/src/Writer.php b/src/Writer.php index 1e04ef38..4d94db84 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -24,6 +24,8 @@ use function gettype; use function implode; use function is_iterable; +use function preg_match; +use function preg_quote; use function sprintf; use function str_replace; use function strlen; diff --git a/tests/RFC4180IteratorTest.php b/tests/RFC4180IteratorTest.php index 5f21a5af..d5357fa1 100644 --- a/tests/RFC4180IteratorTest.php +++ b/tests/RFC4180IteratorTest.php @@ -19,6 +19,7 @@ use PHPUnit\Framework\TestCase; use SplTempFileObject; use TypeError; +use function iterator_to_array; /** * @group reader From e06361fd7b772bef55a138cb6c43f4684cfc4d82 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Wed, 19 Sep 2018 15:32:00 +0200 Subject: [PATCH 054/858] Imrpove PR documentation and CHANGELOG --- CHANGELOG.md | 3 +- docs/9.0/interoperability/rfc4180-field.md | 2 +- docs/9.0/writer/index.md | 6 ++-- src/AbstractCsv.php | 2 +- src/RFC4180Iterator.php | 37 ++++++++++++---------- 5 files changed, 28 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee1ed2c8..57f3bee3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,12 +10,13 @@ All Notable changes to `Csv` will be documented in this file ### Deprecated -- `League\Csv\RFC4180Field` use `Writer::insertXXX` methods instead. +- `League\Csv\RFC4180Field` use `AbstractCSV::setEscape` method with an empty string instead. ### Fixed - `AbstractCSV::setEscape` now accepts the empty string like `fputcsv` and `fgetcsv` - `Writer::insertOne` fixes throwing exception when record can not be inserted +- `XMLConverter` convert to string the record value to avoid PHP warning on `null` value - Internal `Stream::fwrite` improved - Internal `Abstract::__construct` correctly initializes properties diff --git a/docs/9.0/interoperability/rfc4180-field.md b/docs/9.0/interoperability/rfc4180-field.md index 11d87f30..ff534980 100644 --- a/docs/9.0/interoperability/rfc4180-field.md +++ b/docs/9.0/interoperability/rfc4180-field.md @@ -5,7 +5,7 @@ title: CSV document interoperability # RFC4180 Field compliance -

This class is deprecated as of version 9.2.0. Please use directly the Writer::insertXXX methods with the empty escape character argument instead.

+

This class is deprecated as of version 9.2.0. Please use directly the AbstractCsv::setEscape method with the empty escape character argument instead.

~~~php Starting with 9.2.0 if you can provide an empty string for the escape character to enable better RFC4180 compliance.

+

Starting with 9.2.0 you can provide an empty string for the escape character to enable better RFC4180 compliance.

~~~php delimiter, $line, 2) + [1 => false]; - - //remove line breaks characters as per RFC4180 if (false === $line) { - $content = rtrim($content, "\r\n"); + return trim(rtrim($content, "\r\n"), $this->trim_mask); } - //remove whitespaces as per RFC4180 return trim($content, $this->trim_mask); } /** - * Extract field with enclosure. + * Extract field with enclosure as per RFC4180. + * + * - Leading and trailing whitespaces are preserved because the field + * is enclosed. + * - The field content can spread on multiple document lines. + * - Double enclosure character muse be replaced by single enclosure character. + * - Trailing line break are remove if they are not part of the field content. + * - Invalid field do not throw as per fgetcsv behavior. * * @param bool|string $line * @@ -166,37 +176,32 @@ private function extractField(&$line) */ private function extractFieldEnclosed(&$line) { - //remove the starting enclosure char to ease explode usage + //remove the starting enclosure character if present if ($line[0] ?? '' === $this->enclosure) { $line = substr($line, 1); } $content = ''; - //cover multiline field do { - //explode the line on the next enclosure character if any list($buffer, $line) = explode($this->enclosure, $line, 2) + [1 => false]; $content .= $buffer; } while (false === $line && $this->document->valid() && false !== ($line = $this->document->fgets())); - //decode the field content as per RFC4180 $content = str_replace($this->double_enclosure, $this->enclosure, $content); - - //remove line breaks characters as per RFC4180 if (in_array($line, self::FIELD_BREAKS, true)) { $line = false; return rtrim($content, "\r\n"); } - //the field data is extracted since we have a delimiter - if (($line[0] ?? '') === $this->delimiter) { + $char = $line[0] ?? ''; + if ($char === $this->delimiter) { $line = substr($line, 1); return $content; } //handles enclosure as per RFC4180 or malformed CSV like fgetcsv - return $content.($line[0] ?? '').$this->extractFieldEnclosed($line); + return $content.$char.$this->extractFieldEnclosed($line); } } From d4f6a980c9aeec9c4bf137c6756ef73fcda63c5e Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Wed, 19 Sep 2018 15:35:07 +0200 Subject: [PATCH 055/858] update travis to include PHP7.3 --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index f04b472a..cbe0cf5d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,8 @@ matrix: env: COLLECT_COVERAGE=true VALIDATE_CODING_STYLE=true RUN_PHPSTAN=true IGNORE_PLATFORMS=false - php: 7.2 env: COLLECT_COVERAGE=true VALIDATE_CODING_STYLE=false RUN_PHPSTAN=false IGNORE_PLATFORMS=false + - php: 7.3 + env: COLLECT_COVERAGE=false VALIDATE_CODING_STYLE=false RUN_PHPSTAN=false IGNORE_PLATFORMS=false - php: nightly env: COLLECT_COVERAGE=false VALIDATE_CODING_STYLE=false RUN_PHPSTAN=false IGNORE_PLATFORMS=true allow_failures: From f9ccec50c397a42948d26b091a9aa1ffd4709cdd Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Wed, 19 Sep 2018 15:44:58 +0200 Subject: [PATCH 056/858] revert travis PHP7.3 --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index cbe0cf5d..f04b472a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,8 +10,6 @@ matrix: env: COLLECT_COVERAGE=true VALIDATE_CODING_STYLE=true RUN_PHPSTAN=true IGNORE_PLATFORMS=false - php: 7.2 env: COLLECT_COVERAGE=true VALIDATE_CODING_STYLE=false RUN_PHPSTAN=false IGNORE_PLATFORMS=false - - php: 7.3 - env: COLLECT_COVERAGE=false VALIDATE_CODING_STYLE=false RUN_PHPSTAN=false IGNORE_PLATFORMS=false - php: nightly env: COLLECT_COVERAGE=false VALIDATE_CODING_STYLE=false RUN_PHPSTAN=false IGNORE_PLATFORMS=true allow_failures: From 99610588632fb45a7fd3fed049f109bf6f2cd572 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Wed, 19 Sep 2018 20:36:03 +0200 Subject: [PATCH 057/858] bug fix RFC4180 Iterator --- src/RFC4180Iterator.php | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/RFC4180Iterator.php b/src/RFC4180Iterator.php index 31fe74c6..633a5f72 100644 --- a/src/RFC4180Iterator.php +++ b/src/RFC4180Iterator.php @@ -48,7 +48,7 @@ final class RFC4180Iterator implements IteratorAggregate /** * @internal */ - const FIELD_BREAKS = [false, "\r", "\r\n", "\n", '']; + const FIELD_BREAKS = [false, '', "\r\n", "\n", "\r"]; /** * @var SplFileObject|Stream @@ -65,11 +65,6 @@ final class RFC4180Iterator implements IteratorAggregate */ private $enclosure; - /** - * @var string - */ - private $double_enclosure; - /** * @var string */ @@ -102,7 +97,6 @@ public function getIterator() { //initialisation list($this->delimiter, $this->enclosure, ) = $this->document->getCsvControl(); - $this->double_enclosure = $this->enclosure.$this->enclosure; $this->trim_mask = str_replace([$this->delimiter, $this->enclosure], '', " \t\0\x0B"); $this->document->setFlags(0); $this->document->rewind(); @@ -177,7 +171,7 @@ private function extractField(&$line) private function extractFieldEnclosed(&$line) { //remove the starting enclosure character if present - if ($line[0] ?? '' === $this->enclosure) { + if (($line[0] ?? '') === $this->enclosure) { $line = substr($line, 1); } @@ -187,7 +181,6 @@ private function extractFieldEnclosed(&$line) $content .= $buffer; } while (false === $line && $this->document->valid() && false !== ($line = $this->document->fgets())); - $content = str_replace($this->double_enclosure, $this->enclosure, $content); if (in_array($line, self::FIELD_BREAKS, true)) { $line = false; @@ -195,13 +188,19 @@ private function extractFieldEnclosed(&$line) } $char = $line[0] ?? ''; + //handle end of content by delimiter if ($char === $this->delimiter) { $line = substr($line, 1); return $content; } - //handles enclosure as per RFC4180 or malformed CSV like fgetcsv - return $content.$char.$this->extractFieldEnclosed($line); + //handles double quoted data + if ($char === $this->enclosure) { + return $content.$char.$this->extractFieldEnclosed($line); + } + + //handles malformed CSV like fgetcsv by skipping the enclosure character + return $content.$this->extractFieldEnclosed($line); } } From 922f3e2b7f9ade7c39b6c738cc1ac0565e375943 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Wed, 19 Sep 2018 22:49:16 +0200 Subject: [PATCH 058/858] Improve RFC4180 parser --- src/RFC4180Iterator.php | 72 +++++++++++++++++------------------ tests/RFC4180IteratorTest.php | 28 ++++++++++++++ 2 files changed, 64 insertions(+), 36 deletions(-) diff --git a/src/RFC4180Iterator.php b/src/RFC4180Iterator.php index 633a5f72..29a61442 100644 --- a/src/RFC4180Iterator.php +++ b/src/RFC4180Iterator.php @@ -91,7 +91,12 @@ public function __construct($document) /** * @inheritdoc * - * Converts the stream into a CSV record iterator + * Converts the stream into a CSV record iterator by extracting records one by one + * + * The returned record array is similar to the returned value of fgetcsv + * + * - If the line is empty the record will be an array with a single value equals to null + * - Otherwise the array contains strings. */ public function getIterator() { @@ -101,38 +106,25 @@ public function getIterator() $this->document->setFlags(0); $this->document->rewind(); do { - yield $this->extractRecord($this->document->fgets()); + $record = []; + $line = $this->document->fgets(); + do { + $method = 'extractField'; + if (($line[0] ?? '') === $this->enclosure) { + $method = 'extractFieldEnclosed'; + } + $record[] = $this->$method($line); + } while (false !== $line); + + yield $record; } while ($this->document->valid()); } - /** - * Extract a record from the Stream document. - * - * The return array is similar as to the returned value of fgetcsv - * If this the an empty line the record will be an array with a single value - * equals to null otherwise the array contains string data. - * - * @param string|bool $line - */ - private function extractRecord($line): array - { - $record = []; - do { - $method = 'extractField'; - if (($line[0] ?? '') === $this->enclosure) { - $method = 'extractFieldEnclosed'; - } - $record[] = $this->$method($line); - } while (false !== $line); - - return $record; - } - /** * Extract field without enclosure as per RFC4180. * - * Leading and trailing whitespaces are trimmed because the field - * is not enclosed. trailing line-breaks are also removed. + * - Leading and trailing whitespaces must be removed. + * - trailing line-breaks must be removed. * * @param bool|string $line * @@ -157,11 +149,10 @@ private function extractField(&$line) /** * Extract field with enclosure as per RFC4180. * - * - Leading and trailing whitespaces are preserved because the field - * is enclosed. - * - The field content can spread on multiple document lines. - * - Double enclosure character muse be replaced by single enclosure character. - * - Trailing line break are remove if they are not part of the field content. + * - Field content can spread on multiple document lines. + * - Content inside enclosure must be preserved. + * - Double enclosure sequence must be replaced by single enclosure character. + * - Trailing line break must be removed if they are not part of the field content. * - Invalid field do not throw as per fgetcsv behavior. * * @param bool|string $line @@ -176,10 +167,19 @@ private function extractFieldEnclosed(&$line) } $content = ''; - do { + while (false !== $line) { list($buffer, $line) = explode($this->enclosure, $line, 2) + [1 => false]; $content .= $buffer; - } while (false === $line && $this->document->valid() && false !== ($line = $this->document->fgets())); + if (false !== $line) { + break; + } + + if (!$this->document->valid()) { + break; + } + + $line = $this->document->fgets(); + } if (in_array($line, self::FIELD_BREAKS, true)) { $line = false; @@ -188,7 +188,7 @@ private function extractFieldEnclosed(&$line) } $char = $line[0] ?? ''; - //handle end of content by delimiter + //handles end of content by delimiter if ($char === $this->delimiter) { $line = substr($line, 1); @@ -200,7 +200,7 @@ private function extractFieldEnclosed(&$line) return $content.$char.$this->extractFieldEnclosed($line); } - //handles malformed CSV like fgetcsv by skipping the enclosure character + //handles malformed CSV like fgetcsv return $content.$this->extractFieldEnclosed($line); } } diff --git a/tests/RFC4180IteratorTest.php b/tests/RFC4180IteratorTest.php index d5357fa1..da4751f9 100644 --- a/tests/RFC4180IteratorTest.php +++ b/tests/RFC4180IteratorTest.php @@ -27,6 +27,9 @@ */ class RFC4180IteratorTest extends TestCase { + /** + * @covers ::__construct + */ public function testConstructorThrowsTypeErrorWithUnknownDocument() { self::expectException(TypeError::class); @@ -35,7 +38,10 @@ public function testConstructorThrowsTypeErrorWithUnknownDocument() /** * @covers \League\Csv\Stream::fgets + * @covers ::__construct * @covers ::getIterator + * @covers ::extractField + * @covers ::extractFieldEnclosed */ public function testWorksWithMultiLines() { @@ -60,6 +66,9 @@ public function testWorksWithMultiLines() /** * @covers \League\Csv\Stream::fgets + * @covers ::getIterator + * @covers ::extractField + * @covers ::extractFieldEnclosed */ public function testWorksWithMultiLinesWithDifferentDelimiter() { @@ -84,6 +93,11 @@ public function testWorksWithMultiLinesWithDifferentDelimiter() self::assertSame($multiline, $data[4][3]); } + /** + * @covers ::getIterator + * @covers ::extractField + * @covers ::extractFieldEnclosed + */ public function testKeepEmptyLines() { $source = << Date: Wed, 19 Sep 2018 23:06:35 +0200 Subject: [PATCH 059/858] improve parser --- src/CharsetConverter.php | 2 +- src/EncloseField.php | 2 +- src/RFC4180Field.php | 2 +- src/RFC4180Iterator.php | 5 ----- src/Reader.php | 2 +- 5 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/CharsetConverter.php b/src/CharsetConverter.php index 5530fc81..127a0881 100644 --- a/src/CharsetConverter.php +++ b/src/CharsetConverter.php @@ -96,7 +96,7 @@ public static function register() { $filtername = self::FILTERNAME.'.*'; if (!in_array($filtername, stream_get_filters(), true)) { - stream_filter_register($filtername, __CLASS__); + stream_filter_register($filtername, self::class); } } diff --git a/src/EncloseField.php b/src/EncloseField.php index 68b165b3..e65c5f61 100644 --- a/src/EncloseField.php +++ b/src/EncloseField.php @@ -84,7 +84,7 @@ public static function getFiltername(): string public static function register() { if (!in_array(self::FILTERNAME, stream_get_filters(), true)) { - stream_filter_register(self::FILTERNAME, __CLASS__); + stream_filter_register(self::FILTERNAME, self::class); } } diff --git a/src/RFC4180Field.php b/src/RFC4180Field.php index b10efb8b..6863f9e7 100644 --- a/src/RFC4180Field.php +++ b/src/RFC4180Field.php @@ -137,7 +137,7 @@ public static function addFormatterTo(Writer $csv, string $whitespace_replace): public static function register() { if (!in_array(self::FILTERNAME, stream_get_filters(), true)) { - stream_filter_register(self::FILTERNAME, __CLASS__); + stream_filter_register(self::FILTERNAME, self::class); } } diff --git a/src/RFC4180Iterator.php b/src/RFC4180Iterator.php index 29a61442..bb48d9a2 100644 --- a/src/RFC4180Iterator.php +++ b/src/RFC4180Iterator.php @@ -173,11 +173,6 @@ private function extractFieldEnclosed(&$line) if (false !== $line) { break; } - - if (!$this->document->valid()) { - break; - } - $line = $this->document->fgets(); } diff --git a/src/Reader.php b/src/Reader.php index d4c9891c..55dcfca0 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -219,7 +219,7 @@ public function __call($method, array $arguments) return (new ResultSet($this->getRecords(), $this->getHeader()))->$method(...$arguments); } - throw new BadMethodCallException(sprintf('%s::%s() method does not exist', __CLASS__, $method)); + throw new BadMethodCallException(sprintf('%s::%s() method does not exist', self::class, $method)); } /** From 8cb273ca282aed7a1572cce2a98a4ccf1e5ee441 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Fri, 21 Sep 2018 12:41:03 +0200 Subject: [PATCH 060/858] internal code bug fix --- CHANGELOG.md | 4 +++- src/Reader.php | 4 ++-- src/Stream.php | 2 +- src/Writer.php | 4 ++-- tests/StreamTest.php | 3 ++- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57f3bee3..aa392e76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,11 +14,13 @@ All Notable changes to `Csv` will be documented in this file ### Fixed +- `AbstractCSV::__construct` correctly initializes properties +- `AbstractCSV::createFromString` named constructor default argument is now the empty string - `AbstractCSV::setEscape` now accepts the empty string like `fputcsv` and `fgetcsv` - `Writer::insertOne` fixes throwing exception when record can not be inserted - `XMLConverter` convert to string the record value to avoid PHP warning on `null` value - Internal `Stream::fwrite` improved -- Internal `Abstract::__construct` correctly initializes properties +- Internal `Stream::__destruct` no longer emit warning on invalid stream filter removal. ### Removed diff --git a/src/Reader.php b/src/Reader.php index 55dcfca0..1069fe90 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -83,7 +83,7 @@ class Reader extends AbstractCsv implements Countable, IteratorAggregate, JsonSe */ public static function createFromPath(string $path, string $open_mode = 'r', $context = null) { - return new static(Stream::createFromPath($path, $open_mode, $context)); + return parent::createFromPath($path, $open_mode, $context); } /** @@ -219,7 +219,7 @@ public function __call($method, array $arguments) return (new ResultSet($this->getRecords(), $this->getHeader()))->$method(...$arguments); } - throw new BadMethodCallException(sprintf('%s::%s() method does not exist', self::class, $method)); + throw new BadMethodCallException(sprintf('%s::%s() method does not exist', static::class, $method)); } /** diff --git a/src/Stream.php b/src/Stream.php index b4a3498c..412824d3 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -150,7 +150,7 @@ public function __construct($resource) public function __destruct() { $walker = function ($filter): bool { - return stream_filter_remove($filter); + return @stream_filter_remove($filter); }; array_walk_recursive($this->filters, $walker); diff --git a/src/Writer.php b/src/Writer.php index 4d94db84..9ef46c6b 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -99,8 +99,8 @@ class Writer extends AbstractCsv protected function resetProperties() { parent::resetProperties(); - $characters = "\s|".preg_quote($this->delimiter, '/').'|'.preg_quote($this->enclosure, '/'); - $this->rfc4180_regexp = '/['.$characters.']/x'; + $characters = preg_quote($this->delimiter, '/').'|'.preg_quote($this->enclosure, '/'); + $this->rfc4180_regexp = '/[\s|'.$characters.']/x'; $this->rfc4180_enclosure = $this->enclosure.$this->enclosure; } diff --git a/tests/StreamTest.php b/tests/StreamTest.php index 4b29e18b..255a75ff 100644 --- a/tests/StreamTest.php +++ b/tests/StreamTest.php @@ -84,6 +84,7 @@ public function testCreateStreamFromPath() /** * @covers ::createFromPath * @covers ::current + * @covers ::getCurrentRecord */ public function testCreateStreamFromPathWithContext() { @@ -102,7 +103,7 @@ public function testCreateStreamFromPathWithContext() 'r+', stream_context_create([StreamWrapper::PROTOCOL => ['stream' => $fp]]) ); - $stream->setFlags(SplFileObject::READ_AHEAD); + $stream->setFlags(SplFileObject::READ_AHEAD | SplFileObject::READ_CSV); $stream->rewind(); self::assertInternalType('array', $stream->current()); } From 49c64b4f76b5a1ef6c04eba08af712a98dc6a80e Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Fri, 21 Sep 2018 13:18:53 +0200 Subject: [PATCH 061/858] Improve github experience --- .github/ISSUE_TEMPLATE.md | 22 -------------- .github/ISSUE_TEMPLATE/Bug_Report.md | 30 ++++++++++++++++++++ .github/ISSUE_TEMPLATE/Feature_Request.md | 16 +++++++++++ .github/ISSUE_TEMPLATE/Question.md | 14 +++++++++ .github/PULL_REQUEST_TEMPLATE.md | 25 ---------------- .github/PULL_REQUEST_TEMPLATE/Bug_Fix.md | 18 ++++++++++++ .github/PULL_REQUEST_TEMPLATE/New_Feature.md | 26 +++++++++++++++++ 7 files changed, 104 insertions(+), 47 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/ISSUE_TEMPLATE/Bug_Report.md create mode 100644 .github/ISSUE_TEMPLATE/Feature_Request.md create mode 100644 .github/ISSUE_TEMPLATE/Question.md delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/PULL_REQUEST_TEMPLATE/Bug_Fix.md create mode 100644 .github/PULL_REQUEST_TEMPLATE/New_Feature.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index a49d3aa5..00000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,22 +0,0 @@ -## Issue summary - -_(Please explain in plain english your issue/feature request)_ - -### System informations - -_(In case of a bug report Please complete the table below)_ - -| Information | Description | -|--------------|---------| -| League\Csv version | | -| PHP/HHVM version | | -| OS Platform | | - - -## Standalone code, or other way to reproduce the problem - -_(Please complete the text below to help us fix the issue)_ - -### Expected result - -### Actual result diff --git a/.github/ISSUE_TEMPLATE/Bug_Report.md b/.github/ISSUE_TEMPLATE/Bug_Report.md new file mode 100644 index 00000000..39a90e23 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Bug_Report.md @@ -0,0 +1,30 @@ +--- +name: 🐛 Bug Report +about: Something is broken? 🔨 +--- + +### Bug Report + +_(Fill in the relevant information below to help triage your issue.)_ + +| Information | Description | +|--------------|---------| +| Version | | +| PHP version | | +| OS Platform | | + +#### Summary + +_(Please explain in plain english your bug)_ + +#### Standalone code, or other way to reproduce the problem + +_(Please complete the text below to help us fix the issue)_ + +#### Expected result + +_(What was the expected (correct) behavior?)_ + +#### Actual result + +_(What is the current (buggy) behavior?)_ \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/Feature_Request.md b/.github/ISSUE_TEMPLATE/Feature_Request.md new file mode 100644 index 00000000..42792c22 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Feature_Request.md @@ -0,0 +1,16 @@ +--- +name: 🎉 Feature Request +about: Do you have a new feature in mind? +--- + +### Feature Request + +| Q | A +|------------ | ------ +| New Feature | yes +| BC Break | yes/no + + +#### Proposal + +_(Please explain in plain english the feature you would like to see implemented.)_ diff --git a/.github/ISSUE_TEMPLATE/Question.md b/.github/ISSUE_TEMPLATE/Question.md new file mode 100644 index 00000000..9b58810e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Question.md @@ -0,0 +1,14 @@ +--- +name: ❓ Question +about: Are you unsure about something? +--- + +_(Fill in the relevant information below to help triage your issue.)_ + +| Q | A +|------------ | ------ +| Version | x.y.z + +### Question + +_(Please explain in plain english your question)_ \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index c0338145..00000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,25 +0,0 @@ -## Introduction - -_(Please explain the library current status regarding your PR)_ - -## Proposal - -### Describe the new/upated/fixed feature - -_(Please explain your proposal)_ - -### Backward Incompatible Changes - -_(Describe if they exists BC break)_ - -### Targeted release version - -_(Indicate the release version targeted for your PR)_ - -### PR Impact - -_(Describe the PR impact on the current public API)_ - -## Open issues - -_(Describe possible open issues and/or future scope if any depending on this RFC acceptance)_ diff --git a/.github/PULL_REQUEST_TEMPLATE/Bug_Fix.md b/.github/PULL_REQUEST_TEMPLATE/Bug_Fix.md new file mode 100644 index 00000000..618372ba --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/Bug_Fix.md @@ -0,0 +1,18 @@ +--- +name: 🐛 Bug Report +about: Something is broken and you have a failing Unit Test? 🔨 +--- + +### Bug Report + +_(Fill in the relevant information below to help triage your issue.)_ + +| Information | Description | +|--------------|---------| +| Version | | +| PHP version | | +| OS Platform | | + +#### Summary + +_(Please explain in plain english the failing test)_ \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE/New_Feature.md b/.github/PULL_REQUEST_TEMPLATE/New_Feature.md new file mode 100644 index 00000000..ec3e0b5b --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/New_Feature.md @@ -0,0 +1,26 @@ +--- +name: 🎉 New Feature +about: You have implemented some neat idea that you want to make part of League\Csv? 🎩 +--- + + + +### New Feature + +_(Fill in the relevant information below to help triage your issue.)_ + +| Q | A +|------------ | ------ +| New Feature | yes/no +| BC Break | yes/no + + +#### Proposal + +_(Please explain in plain english the feature you would like to see implemented.)_ From 12ea542524d3d29e1166688b21be75e8e7a435f5 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Fri, 21 Sep 2018 13:41:05 +0200 Subject: [PATCH 062/858] Improve documentation --- docs/9.0/connections/instantiation.md | 5 ++++- docs/9.0/interoperability/rfc4180-field.md | 2 +- docs/9.0/writer/index.md | 4 ++-- docs/_config.yml | 2 +- docs/_layouts/default.html | 2 +- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/9.0/connections/instantiation.md b/docs/9.0/connections/instantiation.md index 8f62addf..5e4b9af2 100644 --- a/docs/9.0/connections/instantiation.md +++ b/docs/9.0/connections/instantiation.md @@ -14,7 +14,7 @@ Because CSV documents come in different forms we use named constructors to offer ~~~php Since version 9.2.0 the $str argument default value is the empty string to ease usage.

+ + ## Loading from a file path ~~~php diff --git a/docs/9.0/interoperability/rfc4180-field.md b/docs/9.0/interoperability/rfc4180-field.md index ff534980..64a0cb70 100644 --- a/docs/9.0/interoperability/rfc4180-field.md +++ b/docs/9.0/interoperability/rfc4180-field.md @@ -5,7 +5,7 @@ title: CSV document interoperability # RFC4180 Field compliance -

This class is deprecated as of version 9.2.0. Please use directly the AbstractCsv::setEscape method with the empty escape character argument instead.

+

This class is deprecated as of version 9.2.0. Please use directly the setEscape method with the empty escape character argument instead with the Reader or the Writer object.

~~~php Starting with 9.2.0 you can provide an empty string for the escape character to enable better RFC4180 compliance.

+

Since version 9.2.0 you can provide an empty string for the escape character to enable better RFC4180 compliance.

~~~php getContents(); // """foo""",foo bar,"baz ",foo\bar ~~~ -

The addition of this new feature means the deprecation of [RFC4180Field](/9.0/interoperability/rfc4180-field/).

+

The addition of this new feature means the deprecation of RFC4180Field.

You still need to set the newline sequence as shown below

diff --git a/docs/_config.yml b/docs/_config.yml index 99010eb8..da318906 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,4 +1,4 @@ --- permalink: pretty -gems: +plugins: - jekyll-redirect-from \ No newline at end of file diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index 7d86fc39..e8a79492 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -4,7 +4,7 @@ - {% assign version = page.url | remove_first: "/" | split: "/"" | first %} + {% assign version = page.url | remove_first: "/" | split: "/" | first %} {% if version == '' or version == 'upgrading' %} {% assign version = site.data.project.releases.current.version %} From 472e611baafe9a1582fba47f455013a9f3dfaf7b Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Fri, 21 Sep 2018 13:49:32 +0200 Subject: [PATCH 063/858] fix Writer::insertOne example with setEscape --- docs/9.0/writer/index.md | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/docs/9.0/writer/index.md b/docs/9.0/writer/index.md index 36d35ce6..86b92ebd 100644 --- a/docs/9.0/writer/index.md +++ b/docs/9.0/writer/index.md @@ -87,18 +87,15 @@ try { use League\Csv\Writer; -$records = [ - ['foo', 'foo bar', 'baz '], - -]; +$record = ['"foo"', 'foo bar', 'baz ', 'foo\\"bar']; -$writer = Writer::createFromString(''); -$writer->insertOne(['"foo"', 'foo bar', 'baz ', 'foo\\bar']); +$writer = Writer::createFromString(); +$writer->insertOne($record); $writer->setEscape(''); -$writer->insertOne(['"foo"', 'foo bar', 'baz ', 'foo\\bar']); +$writer->insertOne($record); echo $writer->getContents(); -// """foo""","foo bar","baz ","foo\bar" -// """foo""",foo bar,"baz ",foo\bar +// """foo""","foo bar","baz ","foo\"bar" +// """foo""","foo bar","baz ","foo\""bar" ~~~

The addition of this new feature means the deprecation of RFC4180Field.

From c02a942cec3698cb1507b8cfde205c529ec001fe Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Fri, 21 Sep 2018 13:59:52 +0200 Subject: [PATCH 064/858] update controls documentation page --- docs/9.0/connections/controls.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/9.0/connections/controls.md b/docs/9.0/connections/controls.md index 49dae818..9ba88085 100644 --- a/docs/9.0/connections/controls.md +++ b/docs/9.0/connections/controls.md @@ -7,8 +7,7 @@ title: Csv character controls To correctly parse a CSV document you are required to set the character controls to be used by the `Reader` or the `Writer` object. -

On error the setter methods will throw a Exception exception if the submitted string length is not equal to 1.

- +

Setter methods will throw a Exception exception if the submitted string length is not equal to 1 byte.

## The delimiter character. @@ -85,7 +84,7 @@ $escape = $csv->getEscape(); //returns "\"

The default escape character is \.

-

Starting with 9.2.0 you can provide an empty string for the escape character to enable better RFC4180 compliance.

+

Since version 9.2.0 you can provide an empty string for the escape character to enable better RFC4180 compliance.

~~~php Date: Fri, 21 Sep 2018 14:55:00 +0200 Subject: [PATCH 065/858] update documentation layout --- docs/_layouts/default.html | 37 ++++++++------------------- docs/_layouts/homepage.html | 50 ++++++++++-------------------------- docs/_layouts/redirect.html | 51 ++++++++++--------------------------- 3 files changed, 36 insertions(+), 102 deletions(-) diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index e8a79492..87a0a0fe 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -35,19 +35,17 @@ {% endif %} +{% if site.data.project.google_analytics_tracking_id %} + + +{% endif %} - -
- - The League of Extraordinary Packages - -

Our Packages:

-
    - -
-
-
- - Presented by The League of Extraordinary Packages -
@@ -123,20 +118,8 @@

{{ section[0] }}

Site design by Jonathan Reinink. - + - -{% if site.data.project.google_analytics_tracking_id %} - -{% endif %} - \ No newline at end of file diff --git a/docs/_layouts/homepage.html b/docs/_layouts/homepage.html index 5e8095ed..4e21f91d 100644 --- a/docs/_layouts/homepage.html +++ b/docs/_layouts/homepage.html @@ -5,37 +5,25 @@ {{ site.data.project.tagline }} - {{ site.data.project.title }} - {% if site.data.project.description %} - - {% endif %} - {% if site.data.images.favicon %} - - {% else %} - - {% endif %} - {% if site.data.images.apple_touch %} - - {% else %} - - {% endif %} + + + +{% if site.data.project.google_analytics_tracking_id %} + + +{% endif %} -
- - The League of Extraordinary Packages - -

Our Packages:

-
    - -
-
-
- Presented by The League of Extraordinary Packages

{{ site.data.project.title }}

{{ site.data.project.tagline }}

{{ site.data.project.composer }}

@@ -144,19 +132,7 @@

Questions?

Site design by Jonathan Reinink. - + -{% if site.data.project.google_analytics_tracking_id %} - -{% endif %} - \ No newline at end of file diff --git a/docs/_layouts/redirect.html b/docs/_layouts/redirect.html index 5343a08d..46769946 100644 --- a/docs/_layouts/redirect.html +++ b/docs/_layouts/redirect.html @@ -6,39 +6,26 @@ {{ site.data.project.tagline }} - {{ site.data.project.title }} - {% if site.data.project.description %} - - {% endif %} - {% if site.data.images.favicon %} - - {% else %} - - {% endif %} - {% if site.data.images.apple_touch %} - - {% else %} - - {% endif %} + + + +{% if site.data.project.google_analytics_tracking_id %} + + +{% endif %} - -
- - The League of Extraordinary Packages - -

Our Packages:

-
    - -
-
-
- Presented by The League of Extraordinary Packages

{{ site.data.project.title }}

{{ site.data.project.tagline }}

{{ site.data.project.composer }}

@@ -70,19 +57,7 @@

Questions?

Site design by Jonathan Reinink. - + -{% if site.data.project.google_analytics_tracking_id %} - -{% endif %} - \ No newline at end of file From 0f0f007044d96a7ecee06a9ef27e27fc6f16ab8b Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Fri, 21 Sep 2018 15:04:31 +0200 Subject: [PATCH 066/858] prepare 9.2.0 release --- .php_cs | 2 +- src/AbstractCsv.php | 2 +- src/ByteSequence.php | 2 +- src/CannotInsertRecord.php | 2 +- src/CharsetConverter.php | 2 +- src/ColumnConsistency.php | 2 +- src/EncloseField.php | 2 +- src/EscapeFormula.php | 2 +- src/Exception.php | 2 +- src/HTMLConverter.php | 2 +- src/MapIterator.php | 2 +- src/RFC4180Field.php | 2 +- src/RFC4180Iterator.php | 2 +- src/Reader.php | 2 +- src/ResultSet.php | 2 +- src/Statement.php | 2 +- src/Stream.php | 2 +- src/Writer.php | 2 +- src/XMLConverter.php | 2 +- src/functions.php | 2 +- src/functions_include.php | 2 +- tests/ByteSequenceTest.php | 2 +- tests/CannotInsertRecordTest.php | 2 +- tests/CharsetConverterTest.php | 2 +- tests/ColumnConsistencyTest.php | 2 +- tests/CsvTest.php | 2 +- tests/DetectDelimiterTest.php | 2 +- tests/EncloseFieldTest.php | 2 +- tests/EscapeFormulaTest.php | 2 +- tests/HTMLConverterTest.php | 2 +- tests/RFC4180FieldTest.php | 2 +- tests/RFC4180IteratorTest.php | 2 +- tests/ReaderTest.php | 2 +- tests/ResultSetTest.php | 2 +- tests/StreamTest.php | 2 +- tests/StreamWrapper.php | 2 +- tests/WriterTest.php | 2 +- tests/XMLConverterTest.php | 2 +- 38 files changed, 38 insertions(+), 38 deletions(-) diff --git a/.php_cs b/.php_cs index 9d3bb152..4c434e98 100644 --- a/.php_cs +++ b/.php_cs @@ -5,7 +5,7 @@ League.Csv (https://csv.thephpleague.com) @author Ignace Nyamagana Butera @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) -@version 9.1.5 +@version 9.2.0 @link https://github.com/thephpleague/csv For the full copyright and license information, please view the LICENSE diff --git a/src/AbstractCsv.php b/src/AbstractCsv.php index 8284e210..626cf20c 100644 --- a/src/AbstractCsv.php +++ b/src/AbstractCsv.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/ByteSequence.php b/src/ByteSequence.php index 13c837f8..116d0e28 100644 --- a/src/ByteSequence.php +++ b/src/ByteSequence.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/CannotInsertRecord.php b/src/CannotInsertRecord.php index 8abff099..75bc54da 100644 --- a/src/CannotInsertRecord.php +++ b/src/CannotInsertRecord.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/CharsetConverter.php b/src/CharsetConverter.php index 127a0881..c32edc96 100644 --- a/src/CharsetConverter.php +++ b/src/CharsetConverter.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/ColumnConsistency.php b/src/ColumnConsistency.php index c010602e..7410eeff 100644 --- a/src/ColumnConsistency.php +++ b/src/ColumnConsistency.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/EncloseField.php b/src/EncloseField.php index e65c5f61..72f66e5e 100644 --- a/src/EncloseField.php +++ b/src/EncloseField.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/EscapeFormula.php b/src/EscapeFormula.php index cbf8b44e..35872fb9 100644 --- a/src/EscapeFormula.php +++ b/src/EscapeFormula.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/Exception.php b/src/Exception.php index 0ab0a675..73b23179 100644 --- a/src/Exception.php +++ b/src/Exception.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/HTMLConverter.php b/src/HTMLConverter.php index 5f58fc04..b3a2b9db 100644 --- a/src/HTMLConverter.php +++ b/src/HTMLConverter.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/MapIterator.php b/src/MapIterator.php index 2b990881..050b28d1 100644 --- a/src/MapIterator.php +++ b/src/MapIterator.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/RFC4180Field.php b/src/RFC4180Field.php index 6863f9e7..0f03b43f 100644 --- a/src/RFC4180Field.php +++ b/src/RFC4180Field.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/RFC4180Iterator.php b/src/RFC4180Iterator.php index bb48d9a2..34b0b896 100644 --- a/src/RFC4180Iterator.php +++ b/src/RFC4180Iterator.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/Reader.php b/src/Reader.php index 1069fe90..8bf171f9 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/ResultSet.php b/src/ResultSet.php index 909332d6..d97cec58 100644 --- a/src/ResultSet.php +++ b/src/ResultSet.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/Statement.php b/src/Statement.php index d4fab335..0c635ed3 100644 --- a/src/Statement.php +++ b/src/Statement.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/Stream.php b/src/Stream.php index 412824d3..08e1b166 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/Writer.php b/src/Writer.php index 9ef46c6b..59854710 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/XMLConverter.php b/src/XMLConverter.php index 46fb09ac..3bf8e4aa 100644 --- a/src/XMLConverter.php +++ b/src/XMLConverter.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/functions.php b/src/functions.php index 6dce6ba5..85dad6b8 100644 --- a/src/functions.php +++ b/src/functions.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/src/functions_include.php b/src/functions_include.php index fd10f35a..ae1deba8 100644 --- a/src/functions_include.php +++ b/src/functions_include.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/tests/ByteSequenceTest.php b/tests/ByteSequenceTest.php index 8b2c01e9..2f2307e3 100644 --- a/tests/ByteSequenceTest.php +++ b/tests/ByteSequenceTest.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/tests/CannotInsertRecordTest.php b/tests/CannotInsertRecordTest.php index c577d317..f433cf60 100644 --- a/tests/CannotInsertRecordTest.php +++ b/tests/CannotInsertRecordTest.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/tests/CharsetConverterTest.php b/tests/CharsetConverterTest.php index 2fabea55..2fb7c742 100644 --- a/tests/CharsetConverterTest.php +++ b/tests/CharsetConverterTest.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/tests/ColumnConsistencyTest.php b/tests/ColumnConsistencyTest.php index 9dd51e1d..c8dc2358 100644 --- a/tests/ColumnConsistencyTest.php +++ b/tests/ColumnConsistencyTest.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/tests/CsvTest.php b/tests/CsvTest.php index 3d7fd6df..bb83d648 100644 --- a/tests/CsvTest.php +++ b/tests/CsvTest.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/tests/DetectDelimiterTest.php b/tests/DetectDelimiterTest.php index 9ebd0656..f30539d4 100644 --- a/tests/DetectDelimiterTest.php +++ b/tests/DetectDelimiterTest.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/tests/EncloseFieldTest.php b/tests/EncloseFieldTest.php index 88a8156a..911ea15d 100644 --- a/tests/EncloseFieldTest.php +++ b/tests/EncloseFieldTest.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/tests/EscapeFormulaTest.php b/tests/EscapeFormulaTest.php index c926c9fc..b7428ab5 100644 --- a/tests/EscapeFormulaTest.php +++ b/tests/EscapeFormulaTest.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/tests/HTMLConverterTest.php b/tests/HTMLConverterTest.php index 3a172f6a..b9ea4d84 100644 --- a/tests/HTMLConverterTest.php +++ b/tests/HTMLConverterTest.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/tests/RFC4180FieldTest.php b/tests/RFC4180FieldTest.php index 75d83fb3..6c14c8cf 100644 --- a/tests/RFC4180FieldTest.php +++ b/tests/RFC4180FieldTest.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/tests/RFC4180IteratorTest.php b/tests/RFC4180IteratorTest.php index da4751f9..e58388d3 100644 --- a/tests/RFC4180IteratorTest.php +++ b/tests/RFC4180IteratorTest.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/tests/ReaderTest.php b/tests/ReaderTest.php index b4f491fc..99fc33f7 100644 --- a/tests/ReaderTest.php +++ b/tests/ReaderTest.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/tests/ResultSetTest.php b/tests/ResultSetTest.php index 0653f357..01ef330e 100644 --- a/tests/ResultSetTest.php +++ b/tests/ResultSetTest.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/tests/StreamTest.php b/tests/StreamTest.php index 255a75ff..905c5169 100644 --- a/tests/StreamTest.php +++ b/tests/StreamTest.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/tests/StreamWrapper.php b/tests/StreamWrapper.php index 2fe6d73b..9bf541f8 100644 --- a/tests/StreamWrapper.php +++ b/tests/StreamWrapper.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/tests/WriterTest.php b/tests/WriterTest.php index b2440a33..8068ed35 100644 --- a/tests/WriterTest.php +++ b/tests/WriterTest.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE diff --git a/tests/XMLConverterTest.php b/tests/XMLConverterTest.php index 4593b694..42ca5986 100644 --- a/tests/XMLConverterTest.php +++ b/tests/XMLConverterTest.php @@ -5,7 +5,7 @@ * * @author Ignace Nyamagana Butera * @license https://github.com/thephpleague/csv/blob/master/LICENSE (MIT License) - * @version 9.1.5 + * @version 9.2.0 * @link https://github.com/thephpleague/csv * * For the full copyright and license information, please view the LICENSE From 39bb0472d3906870e21d6a0718bdaa13258bb569 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Fri, 21 Sep 2018 15:54:09 +0200 Subject: [PATCH 067/858] improve parsing invalid field content --- src/AbstractCsv.php | 1 - src/RFC4180Iterator.php | 11 ++--------- tests/RFC4180IteratorTest.php | 2 +- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/AbstractCsv.php b/src/AbstractCsv.php index 626cf20c..081fae06 100644 --- a/src/AbstractCsv.php +++ b/src/AbstractCsv.php @@ -47,7 +47,6 @@ abstract class AbstractCsv implements ByteSequence */ protected $stream_filter_mode; - /** * collection of stream filters. * diff --git a/src/RFC4180Iterator.php b/src/RFC4180Iterator.php index 34b0b896..2d19c694 100644 --- a/src/RFC4180Iterator.php +++ b/src/RFC4180Iterator.php @@ -161,7 +161,6 @@ private function extractField(&$line) */ private function extractFieldEnclosed(&$line) { - //remove the starting enclosure character if present if (($line[0] ?? '') === $this->enclosure) { $line = substr($line, 1); } @@ -183,19 +182,13 @@ private function extractFieldEnclosed(&$line) } $char = $line[0] ?? ''; - //handles end of content by delimiter if ($char === $this->delimiter) { $line = substr($line, 1); return $content; } - //handles double quoted data - if ($char === $this->enclosure) { - return $content.$char.$this->extractFieldEnclosed($line); - } - - //handles malformed CSV like fgetcsv - return $content.$this->extractFieldEnclosed($line); + //treat double quote and invalid field content + return $content.'"'.$this->extractFieldEnclosed($line); } } diff --git a/tests/RFC4180IteratorTest.php b/tests/RFC4180IteratorTest.php index e58388d3..d7462d99 100644 --- a/tests/RFC4180IteratorTest.php +++ b/tests/RFC4180IteratorTest.php @@ -168,7 +168,7 @@ public function invalidCsvRecordProvider() ], 'enclosure started but not ended' => [ 'string' => 'Year,Make,Model,Description,"Pri"ce', - 'record' => ['Year', 'Make', 'Model', 'Description', 'Price'], + 'record' => ['Year', 'Make', 'Model', 'Description', 'Pri"ce'], ], ]; } From 677cc46df58449ee026d134abb17b7967bc0e9f5 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Sat, 22 Sep 2018 19:47:59 +0200 Subject: [PATCH 068/858] bug fix hardcoded enclosure character --- src/RFC4180Iterator.php | 11 +++++------ tests/RFC4180IteratorTest.php | 24 ++++++++++++------------ 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/RFC4180Iterator.php b/src/RFC4180Iterator.php index 2d19c694..bc948c62 100644 --- a/src/RFC4180Iterator.php +++ b/src/RFC4180Iterator.php @@ -109,9 +109,9 @@ public function getIterator() $record = []; $line = $this->document->fgets(); do { - $method = 'extractField'; + $method = 'extractFieldContent'; if (($line[0] ?? '') === $this->enclosure) { - $method = 'extractFieldEnclosed'; + $method = 'extractEnclosedFieldContent'; } $record[] = $this->$method($line); } while (false !== $line); @@ -130,7 +130,7 @@ public function getIterator() * * @return null|string */ - private function extractField(&$line) + private function extractFieldContent(&$line) { if (in_array($line, self::FIELD_BREAKS, true)) { $line = false; @@ -159,7 +159,7 @@ private function extractField(&$line) * * @return null|string */ - private function extractFieldEnclosed(&$line) + private function extractEnclosedFieldContent(&$line) { if (($line[0] ?? '') === $this->enclosure) { $line = substr($line, 1); @@ -188,7 +188,6 @@ private function extractFieldEnclosed(&$line) return $content; } - //treat double quote and invalid field content - return $content.'"'.$this->extractFieldEnclosed($line); + return $content.$this->enclosure.$this->extractEnclosedFieldContent($line); } } diff --git a/tests/RFC4180IteratorTest.php b/tests/RFC4180IteratorTest.php index d7462d99..c27975ce 100644 --- a/tests/RFC4180IteratorTest.php +++ b/tests/RFC4180IteratorTest.php @@ -40,8 +40,8 @@ public function testConstructorThrowsTypeErrorWithUnknownDocument() * @covers \League\Csv\Stream::fgets * @covers ::__construct * @covers ::getIterator - * @covers ::extractField - * @covers ::extractFieldEnclosed + * @covers ::extractFieldContent + * @covers ::extractEnclosedFieldContent */ public function testWorksWithMultiLines() { @@ -67,8 +67,8 @@ public function testWorksWithMultiLines() /** * @covers \League\Csv\Stream::fgets * @covers ::getIterator - * @covers ::extractField - * @covers ::extractFieldEnclosed + * @covers ::extractFieldContent + * @covers ::extractEnclosedFieldContent */ public function testWorksWithMultiLinesWithDifferentDelimiter() { @@ -95,8 +95,8 @@ public function testWorksWithMultiLinesWithDifferentDelimiter() /** * @covers ::getIterator - * @covers ::extractField - * @covers ::extractFieldEnclosed + * @covers ::extractFieldContent + * @covers ::extractEnclosedFieldContent */ public function testKeepEmptyLines() { @@ -121,8 +121,8 @@ public function testKeepEmptyLines() /** * @covers ::getIterator - * @covers ::extractField - * @covers ::extractFieldEnclosed + * @covers ::extractFieldContent + * @covers ::extractEnclosedFieldContent */ public function testTrimSpaceWithNotEncloseField() { @@ -139,8 +139,8 @@ public function testTrimSpaceWithNotEncloseField() /** * @covers ::getIterator - * @covers ::extractField - * @covers ::extractFieldEnclosed + * @covers ::extractFieldContent + * @covers ::extractEnclosedFieldContent * * @dataProvider invalidCsvRecordProvider */ @@ -175,8 +175,8 @@ public function invalidCsvRecordProvider() /** * @covers ::getIterator - * @covers ::extractField - * @covers ::extractFieldEnclosed + * @covers ::extractFieldContent + * @covers ::extractEnclosedFieldContent */ public function testDoubleEnclosure() { From 8038d1da0080d7513aaee415c83937174dd073cc Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Sat, 22 Sep 2018 20:33:29 +0200 Subject: [PATCH 069/858] Improve metadoc --- src/AbstractCsv.php | 4 ---- src/ByteSequence.php | 4 ---- src/CannotInsertRecord.php | 4 ---- src/CharsetConverter.php | 6 +----- src/ColumnConsistency.php | 6 +----- src/EncloseField.php | 4 ---- src/EscapeFormula.php | 6 +----- src/Exception.php | 4 ---- src/HTMLConverter.php | 6 +----- src/MapIterator.php | 3 --- src/RFC4180Field.php | 4 ---- src/RFC4180Iterator.php | 8 +------- src/Reader.php | 5 +---- src/ResultSet.php | 4 ---- src/Statement.php | 6 +----- src/Stream.php | 5 +---- src/Writer.php | 4 ---- src/XMLConverter.php | 6 +----- 18 files changed, 9 insertions(+), 80 deletions(-) diff --git a/src/AbstractCsv.php b/src/AbstractCsv.php index 081fae06..b1417bc1 100644 --- a/src/AbstractCsv.php +++ b/src/AbstractCsv.php @@ -33,10 +33,6 @@ /** * An abstract class to enable CSV document loading. - * - * @package League.csv - * @since 4.0.0 - * @author Ignace Nyamagana Butera */ abstract class AbstractCsv implements ByteSequence { diff --git a/src/ByteSequence.php b/src/ByteSequence.php index 116d0e28..4a95b907 100644 --- a/src/ByteSequence.php +++ b/src/ByteSequence.php @@ -16,10 +16,6 @@ /** * Defines constants for common BOM sequences. - * - * @package League.csv - * @since 9.0.0 - * @author Ignace Nyamagana Butera */ interface ByteSequence { diff --git a/src/CannotInsertRecord.php b/src/CannotInsertRecord.php index 75bc54da..f96e5ec8 100644 --- a/src/CannotInsertRecord.php +++ b/src/CannotInsertRecord.php @@ -18,10 +18,6 @@ /** * Thrown when a data is not added to the Csv Document. - * - * @package League.csv - * @since 9.0.0 - * @author Ignace Nyamagana Butera */ class CannotInsertRecord extends Exception { diff --git a/src/CharsetConverter.php b/src/CharsetConverter.php index c32edc96..3c63aacf 100644 --- a/src/CharsetConverter.php +++ b/src/CharsetConverter.php @@ -40,11 +40,7 @@ use function substr; /** - * A class to convert resource stream or tabular data content charset. - * - * @package League.csv - * @since 9.0.0 - * @author Ignace Nyamagana Butera + * Converts resource stream or tabular data content charset. */ class CharsetConverter extends php_user_filter { diff --git a/src/ColumnConsistency.php b/src/ColumnConsistency.php index 7410eeff..ca4d1463 100644 --- a/src/ColumnConsistency.php +++ b/src/ColumnConsistency.php @@ -20,11 +20,7 @@ use function sprintf; /** - * A class to validate column consistency when inserting records into a CSV document. - * - * @package League.csv - * @since 7.0.0 - * @author Ignace Nyamagana Butera + * Validates column consistency when inserting records into a CSV document. */ class ColumnConsistency { diff --git a/src/EncloseField.php b/src/EncloseField.php index 72f66e5e..80bebae0 100644 --- a/src/EncloseField.php +++ b/src/EncloseField.php @@ -32,10 +32,6 @@ * * @see https://tools.ietf.org/html/rfc4180#section-2 * @see https://bugs.php.net/bug.php?id=38301 - * - * @package League.csv - * @since 9.0.0 - * @author Ignace Nyamagana Butera */ class EncloseField extends php_user_filter { diff --git a/src/EscapeFormula.php b/src/EscapeFormula.php index 35872fb9..4cab3661 100644 --- a/src/EscapeFormula.php +++ b/src/EscapeFormula.php @@ -28,13 +28,9 @@ use function sprintf; /** - * A League CSV formatter to tackle CSV Formula Injection. + * A Formatter to tackle CSV Formula Injection. * * @see http://georgemauer.net/2017/10/07/csv-injection.html - * - * @package League.csv - * @since 9.1.0 - * @author Ignace Nyamagana Butera */ class EscapeFormula { diff --git a/src/Exception.php b/src/Exception.php index 73b23179..e5aed1cc 100644 --- a/src/Exception.php +++ b/src/Exception.php @@ -18,10 +18,6 @@ /** * League Csv Base Exception. - * - * @package League.csv - * @since 9.0.0 - * @author Ignace Nyamagana Butera */ class Exception extends \Exception { diff --git a/src/HTMLConverter.php b/src/HTMLConverter.php index b3a2b9db..76aa6688 100644 --- a/src/HTMLConverter.php +++ b/src/HTMLConverter.php @@ -21,11 +21,7 @@ use function preg_match; /** - * A class to convert tabular data into an HTML Table string. - * - * @package League.csv - * @since 9.0.0 - * @author Ignace Nyamagana Butera + * Converts tabular data into an HTML Table string. */ class HTMLConverter { diff --git a/src/MapIterator.php b/src/MapIterator.php index 050b28d1..b74cabc3 100644 --- a/src/MapIterator.php +++ b/src/MapIterator.php @@ -22,9 +22,6 @@ /** * Map value from an iterator before yielding. * - * @package League.csv - * @since 3.3.0 - * @author Ignace Nyamagana Butera * @internal used internally to modify CSV content */ class MapIterator extends IteratorIterator diff --git a/src/RFC4180Field.php b/src/RFC4180Field.php index 0f03b43f..254fc6db 100644 --- a/src/RFC4180Field.php +++ b/src/RFC4180Field.php @@ -40,10 +40,6 @@ * @see AbstractCsv::setEscape * * @see https://tools.ietf.org/html/rfc4180#section-2 - * - * @package League.csv - * @since 9.0.0 - * @author Ignace Nyamagana Butera */ class RFC4180Field extends php_user_filter { diff --git a/src/RFC4180Iterator.php b/src/RFC4180Iterator.php index bc948c62..d422030c 100644 --- a/src/RFC4180Iterator.php +++ b/src/RFC4180Iterator.php @@ -38,9 +38,6 @@ * @see https://tools.ietf.org/html/rfc4180 * @see http://edoceo.com/utilitas/csv-file-format * - * @package League.csv - * @since 9.2.0 - * @author Ignace Nyamagana Butera * @internal used internally to produce RFC4180 compliant records */ final class RFC4180Iterator implements IteratorAggregate @@ -100,7 +97,6 @@ public function __construct($document) */ public function getIterator() { - //initialisation list($this->delimiter, $this->enclosure, ) = $this->document->getCsvControl(); $this->trim_mask = str_replace([$this->delimiter, $this->enclosure], '', " \t\0\x0B"); $this->document->setFlags(0); @@ -156,10 +152,8 @@ private function extractFieldContent(&$line) * - Invalid field do not throw as per fgetcsv behavior. * * @param bool|string $line - * - * @return null|string */ - private function extractEnclosedFieldContent(&$line) + private function extractEnclosedFieldContent(&$line): string { if (($line[0] ?? '') === $this->enclosure) { $line = substr($line, 1); diff --git a/src/Reader.php b/src/Reader.php index 8bf171f9..e0c7fb25 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -41,10 +41,7 @@ use function substr; /** - * A class to select records from a CSV document. - * - * @package League.csv - * @since 3.0.0 + * A class to parse and read records from a CSV document. * * @method array fetchOne(int $nth_record = 0) Returns a single record from the CSV * @method Generator fetchColumn(string|int $column_index) Returns the next value from a single CSV record field diff --git a/src/ResultSet.php b/src/ResultSet.php index d97cec58..96ea9d24 100644 --- a/src/ResultSet.php +++ b/src/ResultSet.php @@ -32,10 +32,6 @@ /** * Represents the result set of a {@link Reader} processed by a {@link Statement}. - * - * @package League.csv - * @since 9.0.0 - * @author Ignace Nyamagana Butera */ class ResultSet implements Countable, IteratorAggregate, JsonSerializable { diff --git a/src/Statement.php b/src/Statement.php index 0c635ed3..b5492e17 100644 --- a/src/Statement.php +++ b/src/Statement.php @@ -24,11 +24,7 @@ use function iterator_to_array; /** - * A Prepared statement to be executed on a {@link Reader} object. - * - * @package League.csv - * @since 9.0.0 - * @author Ignace Nyamagana Butera + * Criteria to filter a {@link Reader} object. */ class Statement { diff --git a/src/Stream.php b/src/Stream.php index 08e1b166..1cbd314f 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -46,11 +46,8 @@ use function strlen; /** - * An object oriented API for a CSV stream resource. + * An object oriented API to handle a PHP stream resource. * - * @package League.csv - * @since 8.2.0 - * @author Ignace Nyamagana Butera * @internal used internally to iterate over a stream resource */ class Stream implements SeekableIterator diff --git a/src/Writer.php b/src/Writer.php index 59854710..614107d9 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -32,10 +32,6 @@ /** * A class to insert records into a CSV Document. - * - * @package League.csv - * @since 4.0.0 - * @author Ignace Nyamagana Butera */ class Writer extends AbstractCsv { diff --git a/src/XMLConverter.php b/src/XMLConverter.php index 3bf8e4aa..d061ae94 100644 --- a/src/XMLConverter.php +++ b/src/XMLConverter.php @@ -27,11 +27,7 @@ use function sprintf; /** - * A class to convert tabular data into a DOMDOcument object. - * - * @package League.csv - * @since 9.0.0 - * @author Ignace Nyamagana Butera + * Converts tabular data into a DOMDOcument object. */ class XMLConverter { From c108c331072aa8b74cbefe2cd12dbe60d458e3f0 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Sun, 23 Sep 2018 20:55:13 +0200 Subject: [PATCH 070/858] bugfix #310 --- src/RFC4180Iterator.php | 6 +++++- src/Reader.php | 3 +-- tests/RFC4180IteratorTest.php | 18 +++++++++++++++++- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/RFC4180Iterator.php b/src/RFC4180Iterator.php index d422030c..1125b59e 100644 --- a/src/RFC4180Iterator.php +++ b/src/RFC4180Iterator.php @@ -182,6 +182,10 @@ private function extractEnclosedFieldContent(&$line): string return $content; } - return $content.$this->enclosure.$this->extractEnclosedFieldContent($line); + if ($char === $this->enclosure) { + return $content.$this->enclosure.$this->extractEnclosedFieldContent($line); + } + + return $content.$this->extractFieldContent($line); } } diff --git a/src/Reader.php b/src/Reader.php index e0c7fb25..c464acf2 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -155,8 +155,7 @@ protected function setHeader(int $offset): array */ protected function seekRow(int $offset) { - $document = $this->getDocument(); - foreach ($document as $index => $record) { + foreach ($this->getDocument() as $index => $record) { if ($offset === $index) { return $record; } diff --git a/tests/RFC4180IteratorTest.php b/tests/RFC4180IteratorTest.php index c27975ce..29dde474 100644 --- a/tests/RFC4180IteratorTest.php +++ b/tests/RFC4180IteratorTest.php @@ -14,6 +14,7 @@ namespace LeagueTest\Csv; +use League\Csv\Reader; use League\Csv\RFC4180Iterator; use League\Csv\Stream; use PHPUnit\Framework\TestCase; @@ -168,7 +169,7 @@ public function invalidCsvRecordProvider() ], 'enclosure started but not ended' => [ 'string' => 'Year,Make,Model,Description,"Pri"ce', - 'record' => ['Year', 'Make', 'Model', 'Description', 'Pri"ce'], + 'record' => ['Year', 'Make', 'Model', 'Description', 'Price'], ], ]; } @@ -199,4 +200,19 @@ public function testDoubleEnclosure() $records = new RFC4180Iterator($stream); self::assertEquals($expected, iterator_to_array($records->getIterator(), false)); } + + /** + * @covers ::getIterator + * @covers ::extractFieldContent + * @covers ::extractEnclosedFieldContent + */ + public function testInvalidCsvParseAsFgetcsv() + { + $str = '"foo"bar",foo"bar'."\r\n".'"foo"'."\r\n".'baz,bar"'; + $csv = Reader::createFromString($str); + $fgetcsv_records = iterator_to_array($csv); + $csv->setEscape(''); + $parser_records = iterator_to_array($csv); + self::assertEquals($fgetcsv_records, $parser_records); + } } From 778a2408ef684ad411cad8baf7e9e551b4113048 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Sun, 23 Sep 2018 22:03:45 +0200 Subject: [PATCH 071/858] improve RFC4180Iterator --- src/RFC4180Iterator.php | 16 +++++++++++++--- src/Reader.php | 4 +--- tests/RFC4180IteratorTest.php | 27 ++++++++++++++++++++++----- 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/RFC4180Iterator.php b/src/RFC4180Iterator.php index 1125b59e..24eb753a 100644 --- a/src/RFC4180Iterator.php +++ b/src/RFC4180Iterator.php @@ -27,6 +27,7 @@ use function rtrim; use function sprintf; use function str_replace; +use function strlen; use function substr; use function trim; @@ -72,7 +73,7 @@ final class RFC4180Iterator implements IteratorAggregate * * @param SplFileObject|Stream $document */ - public function __construct($document) + public function __construct($document, string $delimiter = ',', string $enclosure = '"') { if (!$document instanceof Stream && !$document instanceof SplFileObject) { throw new TypeError(sprintf( @@ -82,7 +83,18 @@ public function __construct($document) )); } + if (1 !== strlen($delimiter)) { + throw new Exception(sprintf('%s() expects delimiter to be a single character %s given', __METHOD__, $delimiter)); + } + + if (1 !== strlen($enclosure)) { + throw new Exception(sprintf('%s() expects enclosure to be a single character %s given', __METHOD__, $enclosure)); + } + $this->document = $document; + $this->delimiter = $delimiter; + $this->enclosure = $enclosure; + $this->trim_mask = str_replace([$this->delimiter, $this->enclosure], '', " \t\0\x0B"); } /** @@ -97,8 +109,6 @@ public function __construct($document) */ public function getIterator() { - list($this->delimiter, $this->enclosure, ) = $this->document->getCsvControl(); - $this->trim_mask = str_replace([$this->delimiter, $this->enclosure], '', " \t\0\x0B"); $this->document->setFlags(0); $this->document->rewind(); do { diff --git a/src/Reader.php b/src/Reader.php index c464acf2..ec8ee00a 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -170,9 +170,7 @@ protected function seekRow(int $offset) protected function getDocument(): Iterator { if ('' === $this->escape && PHP_VERSION_ID < 70400) { - $this->document->setCsvControl($this->delimiter, $this->enclosure); - - return (new RFC4180Iterator($this->document))->getIterator(); + return (new RFC4180Iterator($this->document, $this->delimiter, $this->enclosure))->getIterator(); } $this->document->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY); diff --git a/tests/RFC4180IteratorTest.php b/tests/RFC4180IteratorTest.php index 29dde474..d506fdc0 100644 --- a/tests/RFC4180IteratorTest.php +++ b/tests/RFC4180IteratorTest.php @@ -14,6 +14,7 @@ namespace LeagueTest\Csv; +use League\Csv\Exception; use League\Csv\Reader; use League\Csv\RFC4180Iterator; use League\Csv\Stream; @@ -37,6 +38,24 @@ public function testConstructorThrowsTypeErrorWithUnknownDocument() new RFC4180Iterator([]); } + /** + * @covers ::__construct + */ + public function testConstructorThrowExceptionWithInvalidDelimiter() + { + self::expectException(Exception::class); + new RFC4180Iterator(new SplTempFileObject(), 'toto'); + } + + /** + * @covers ::__construct + */ + public function testConstructorThrowExceptionWithInvalidEnclosure() + { + self::expectException(Exception::class); + new RFC4180Iterator(new SplTempFileObject(), ';', 'é'); + } + /** * @covers \League\Csv\Stream::fgets * @covers ::__construct @@ -87,8 +106,7 @@ public function testWorksWithMultiLinesWithDifferentDelimiter() air| moon roof| loaded EOF; $doc = Stream::createFromString($source); - $doc->setCsvControl('|', "'"); - $iterator = new RFC4180Iterator($doc); + $iterator = new RFC4180Iterator($doc, '|', "'"); self::assertCount(5, $iterator); $data = iterator_to_array($iterator->getIterator(), false); self::assertSame($multiline, $data[4][3]); @@ -195,9 +213,8 @@ public function testDoubleEnclosure() ['Michel;Michele', 'Durand', 'av. de la Ferme, 89', '…'], ]; - $stream = Stream::createFromString($str); - $stream->setCsvControl(';'); - $records = new RFC4180Iterator($stream); + $stream = Stream::createFromString($str); + $records = new RFC4180Iterator($stream, ';'); self::assertEquals($expected, iterator_to_array($records->getIterator(), false)); } From 7e89821243a285bfce822117fb25f9ad8b352ba7 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Mon, 24 Sep 2018 08:28:25 +0200 Subject: [PATCH 072/858] change parser name --- phpstan.tests.neon | 2 +- ...{RFC4180Iterator.php => RFC4180Parser.php} | 2 +- src/Reader.php | 2 +- ...IteratorTest.php => RFC4180ParserTest.php} | 24 +++++++++---------- 4 files changed, 15 insertions(+), 15 deletions(-) rename src/{RFC4180Iterator.php => RFC4180Parser.php} (98%) rename tests/{RFC4180IteratorTest.php => RFC4180ParserTest.php} (90%) diff --git a/phpstan.tests.neon b/phpstan.tests.neon index 11046dd2..90cdb856 100644 --- a/phpstan.tests.neon +++ b/phpstan.tests.neon @@ -10,5 +10,5 @@ parameters: - '#Parameter \#1 \$resource of class League\\Csv\\Stream constructor expects resource, string given.#' - '#Parameter \#1 \$records of method League\\Csv\\CharsetConverter::convert\(\) expects array|Traversable, string given.#' - '#Parameter \#2 \$delimiters of function League\\Csv\\delimiter_detect expects array, array given.#' - - '#Parameter \#1 \$document of class League\\Csv\\RFC4180Iterator constructor expects League\\Csv\\Stream\|SplFileObject, array given.#' + - '#Parameter \#1 \$document of class League\\Csv\\RFC4180Parser constructor expects League\\Csv\\Stream\|SplFileObject, array given.#' reportUnmatchedIgnoredErrors: false diff --git a/src/RFC4180Iterator.php b/src/RFC4180Parser.php similarity index 98% rename from src/RFC4180Iterator.php rename to src/RFC4180Parser.php index 24eb753a..0d6ed2a8 100644 --- a/src/RFC4180Iterator.php +++ b/src/RFC4180Parser.php @@ -41,7 +41,7 @@ * * @internal used internally to produce RFC4180 compliant records */ -final class RFC4180Iterator implements IteratorAggregate +final class RFC4180Parser implements IteratorAggregate { /** * @internal diff --git a/src/Reader.php b/src/Reader.php index ec8ee00a..5f71e7f1 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -170,7 +170,7 @@ protected function seekRow(int $offset) protected function getDocument(): Iterator { if ('' === $this->escape && PHP_VERSION_ID < 70400) { - return (new RFC4180Iterator($this->document, $this->delimiter, $this->enclosure))->getIterator(); + return (new RFC4180Parser($this->document, $this->delimiter, $this->enclosure))->getIterator(); } $this->document->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY); diff --git a/tests/RFC4180IteratorTest.php b/tests/RFC4180ParserTest.php similarity index 90% rename from tests/RFC4180IteratorTest.php rename to tests/RFC4180ParserTest.php index d506fdc0..a435ec40 100644 --- a/tests/RFC4180IteratorTest.php +++ b/tests/RFC4180ParserTest.php @@ -16,7 +16,7 @@ use League\Csv\Exception; use League\Csv\Reader; -use League\Csv\RFC4180Iterator; +use League\Csv\RFC4180Parser; use League\Csv\Stream; use PHPUnit\Framework\TestCase; use SplTempFileObject; @@ -25,9 +25,9 @@ /** * @group reader - * @coversDefaultClass League\Csv\RFC4180Iterator + * @coversDefaultClass League\Csv\RFC4180Parser */ -class RFC4180IteratorTest extends TestCase +class RFC4180ParserTest extends TestCase { /** * @covers ::__construct @@ -35,7 +35,7 @@ class RFC4180IteratorTest extends TestCase public function testConstructorThrowsTypeErrorWithUnknownDocument() { self::expectException(TypeError::class); - new RFC4180Iterator([]); + new RFC4180Parser([]); } /** @@ -44,7 +44,7 @@ public function testConstructorThrowsTypeErrorWithUnknownDocument() public function testConstructorThrowExceptionWithInvalidDelimiter() { self::expectException(Exception::class); - new RFC4180Iterator(new SplTempFileObject(), 'toto'); + new RFC4180Parser(new SplTempFileObject(), 'toto'); } /** @@ -53,7 +53,7 @@ public function testConstructorThrowExceptionWithInvalidDelimiter() public function testConstructorThrowExceptionWithInvalidEnclosure() { self::expectException(Exception::class); - new RFC4180Iterator(new SplTempFileObject(), ';', 'é'); + new RFC4180Parser(new SplTempFileObject(), ';', 'é'); } /** @@ -78,7 +78,7 @@ public function testWorksWithMultiLines() MUST SELL! air, moon roof, loaded EOF; - $iterator = new RFC4180Iterator(Stream::createFromString($source)); + $iterator = new RFC4180Parser(Stream::createFromString($source)); self::assertCount(5, $iterator); $data = iterator_to_array($iterator->getIterator(), false); self::assertSame($multiline, $data[4][3]); @@ -106,7 +106,7 @@ public function testWorksWithMultiLinesWithDifferentDelimiter() air| moon roof| loaded EOF; $doc = Stream::createFromString($source); - $iterator = new RFC4180Iterator($doc, '|', "'"); + $iterator = new RFC4180Parser($doc, '|', "'"); self::assertCount(5, $iterator); $data = iterator_to_array($iterator->getIterator(), false); self::assertSame($multiline, $data[4][3]); @@ -128,7 +128,7 @@ public function testKeepEmptyLines() $rsrc = new SplTempFileObject(); $rsrc->fwrite($source); - $iterator = new RFC4180Iterator($rsrc); + $iterator = new RFC4180Parser($rsrc); self::assertCount(4, $iterator); $data = iterator_to_array($iterator->getIterator(), false); @@ -149,7 +149,7 @@ public function testTrimSpaceWithNotEncloseField() Year,Make,Model,,Description, Price "1997,Ford,E350,"ac, abs, moon", 3000.00 EOF; - $iterator = new RFC4180Iterator(Stream::createFromString($source)); + $iterator = new RFC4180Parser(Stream::createFromString($source)); self::assertCount(2, $iterator); $data = iterator_to_array($iterator->getIterator(), false); self::assertSame(['Year', 'Make', 'Model', '', 'Description', 'Price'], $data[0]); @@ -165,7 +165,7 @@ public function testTrimSpaceWithNotEncloseField() */ public function testHandlingInvalidCSVwithEnclosure(string $string, array $record) { - $iterator = new RFC4180Iterator(Stream::createFromString($string)); + $iterator = new RFC4180Parser(Stream::createFromString($string)); $data = iterator_to_array($iterator->getIterator(), false); self::assertSame($record, $data[0]); } @@ -214,7 +214,7 @@ public function testDoubleEnclosure() ]; $stream = Stream::createFromString($str); - $records = new RFC4180Iterator($stream, ';'); + $records = new RFC4180Parser($stream, ';'); self::assertEquals($expected, iterator_to_array($records->getIterator(), false)); } From eec61a4ed1b0b07e4cbc30707db994500761708d Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Mon, 24 Sep 2018 08:47:10 +0200 Subject: [PATCH 073/858] Improve RFC4180 property validation --- src/RFC4180Parser.php | 46 +++++++++++++++++++++++++------------ tests/RFC4180ParserTest.php | 4 ++++ 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/src/RFC4180Parser.php b/src/RFC4180Parser.php index 0d6ed2a8..061f39e0 100644 --- a/src/RFC4180Parser.php +++ b/src/RFC4180Parser.php @@ -75,26 +75,42 @@ final class RFC4180Parser implements IteratorAggregate */ public function __construct($document, string $delimiter = ',', string $enclosure = '"') { - if (!$document instanceof Stream && !$document instanceof SplFileObject) { - throw new TypeError(sprintf( - 'Expected a %s or an SplFileObject object, % given', - Stream::class, - is_object($document) ? get_class($document) : gettype($document) - )); - } + $this->document = $this->filterDocument($document); + $this->delimiter = $this->filterControl($delimiter, 'delimiter'); + $this->enclosure = $this->filterControl($enclosure, 'enclosure'); + $this->trim_mask = str_replace([$this->delimiter, $this->enclosure], '', " \t\0\x0B"); + } - if (1 !== strlen($delimiter)) { - throw new Exception(sprintf('%s() expects delimiter to be a single character %s given', __METHOD__, $delimiter)); + /** + * Filter the submitted document. + * + * @param SplFileObject|Stream $document + * + * @return SplFileObject|Stream + */ + private function filterDocument($document) + { + if ($document instanceof Stream || $document instanceof SplFileObject) { + return $document; } - if (1 !== strlen($enclosure)) { - throw new Exception(sprintf('%s() expects enclosure to be a single character %s given', __METHOD__, $enclosure)); + throw new TypeError(sprintf( + 'Expected a %s or an SplFileObject object, % given', + Stream::class, + is_object($document) ? get_class($document) : gettype($document) + )); + } + + /** + * Filter the control characters. + */ + private function filterControl(string $control, string $name): string + { + if (1 === strlen($control)) { + return $control; } - $this->document = $document; - $this->delimiter = $delimiter; - $this->enclosure = $enclosure; - $this->trim_mask = str_replace([$this->delimiter, $this->enclosure], '', " \t\0\x0B"); + throw new Exception(sprintf('Expected %s to be a single character %s given', $name, $control)); } /** diff --git a/tests/RFC4180ParserTest.php b/tests/RFC4180ParserTest.php index a435ec40..fa3ecab1 100644 --- a/tests/RFC4180ParserTest.php +++ b/tests/RFC4180ParserTest.php @@ -31,6 +31,7 @@ class RFC4180ParserTest extends TestCase { /** * @covers ::__construct + * @covers ::filterDocument */ public function testConstructorThrowsTypeErrorWithUnknownDocument() { @@ -40,6 +41,7 @@ public function testConstructorThrowsTypeErrorWithUnknownDocument() /** * @covers ::__construct + * @covers ::filterControl */ public function testConstructorThrowExceptionWithInvalidDelimiter() { @@ -49,6 +51,7 @@ public function testConstructorThrowExceptionWithInvalidDelimiter() /** * @covers ::__construct + * @covers ::filterControl */ public function testConstructorThrowExceptionWithInvalidEnclosure() { @@ -59,6 +62,7 @@ public function testConstructorThrowExceptionWithInvalidEnclosure() /** * @covers \League\Csv\Stream::fgets * @covers ::__construct + * @covers ::filterDocument * @covers ::getIterator * @covers ::extractFieldContent * @covers ::extractEnclosedFieldContent From 03e87ae2dc4e4bc9c7449b3e26314e0c121675ed Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Mon, 24 Sep 2018 09:37:52 +0200 Subject: [PATCH 074/858] improve RFC4180Parser internal code --- src/RFC4180Parser.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/RFC4180Parser.php b/src/RFC4180Parser.php index 061f39e0..1cfcf7c9 100644 --- a/src/RFC4180Parser.php +++ b/src/RFC4180Parser.php @@ -95,22 +95,22 @@ private function filterDocument($document) } throw new TypeError(sprintf( - 'Expected a %s or an SplFileObject object, % given', + 'Expected a %s or an SplFileObject object, %s given', Stream::class, is_object($document) ? get_class($document) : gettype($document) )); } /** - * Filter the control characters. + * Filter a control character. */ - private function filterControl(string $control, string $name): string + private function filterControl(string $value, string $name): string { - if (1 === strlen($control)) { - return $control; + if (1 === strlen($value)) { + return $value; } - throw new Exception(sprintf('Expected %s to be a single character %s given', $name, $control)); + throw new Exception(sprintf('Expected %s to be a single character %s given', $name, $value)); } /** From 7974bb5453789a17bcca534499d0ddc31b8c0e68 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Mon, 24 Sep 2018 12:36:53 +0200 Subject: [PATCH 075/858] remove value by reference usage --- src/RFC4180Parser.php | 54 ++++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/src/RFC4180Parser.php b/src/RFC4180Parser.php index 1cfcf7c9..f8a22535 100644 --- a/src/RFC4180Parser.php +++ b/src/RFC4180Parser.php @@ -68,6 +68,11 @@ final class RFC4180Parser implements IteratorAggregate */ private $trim_mask; + /** + * @var string|bool + */ + private $line = false; + /** * New instance. * @@ -129,14 +134,14 @@ public function getIterator() $this->document->rewind(); do { $record = []; - $line = $this->document->fgets(); + $this->line = $this->document->fgets(); do { $method = 'extractFieldContent'; - if (($line[0] ?? '') === $this->enclosure) { + if (($this->line[0] ?? '') === $this->enclosure) { $method = 'extractEnclosedFieldContent'; } - $record[] = $this->$method($line); - } while (false !== $line); + $record[] = $this->$method(); + } while (false !== $this->line); yield $record; } while ($this->document->valid()); @@ -148,20 +153,18 @@ public function getIterator() * - Leading and trailing whitespaces must be removed. * - trailing line-breaks must be removed. * - * @param bool|string $line - * * @return null|string */ - private function extractFieldContent(&$line) + private function extractFieldContent() { - if (in_array($line, self::FIELD_BREAKS, true)) { - $line = false; + if (in_array($this->line, self::FIELD_BREAKS, true)) { + $this->line = false; return null; } - list($content, $line) = explode($this->delimiter, $line, 2) + [1 => false]; - if (false === $line) { + list($content, $this->line) = explode($this->delimiter, $this->line, 2) + [1 => false]; + if (false === $this->line) { return trim(rtrim($content, "\r\n"), $this->trim_mask); } @@ -176,42 +179,41 @@ private function extractFieldContent(&$line) * - Double enclosure sequence must be replaced by single enclosure character. * - Trailing line break must be removed if they are not part of the field content. * - Invalid field do not throw as per fgetcsv behavior. - * - * @param bool|string $line */ - private function extractEnclosedFieldContent(&$line): string + private function extractEnclosedFieldContent(): string { - if (($line[0] ?? '') === $this->enclosure) { - $line = substr($line, 1); + if (($this->line[0] ?? '') === $this->enclosure) { + $this->line = substr($this->line, 1); } $content = ''; - while (false !== $line) { - list($buffer, $line) = explode($this->enclosure, $line, 2) + [1 => false]; + while (false !== $this->line) { + list($buffer, $remainder) = explode($this->enclosure, $this->line, 2) + [1 => false]; $content .= $buffer; - if (false !== $line) { + if (false !== $remainder) { + $this->line = $remainder; break; } - $line = $this->document->fgets(); + $this->line = $this->document->fgets(); } - if (in_array($line, self::FIELD_BREAKS, true)) { - $line = false; + if (in_array($this->line, self::FIELD_BREAKS, true)) { + $this->line = false; return rtrim($content, "\r\n"); } - $char = $line[0] ?? ''; + $char = $this->line[0] ?? ''; if ($char === $this->delimiter) { - $line = substr($line, 1); + $this->line = substr($this->line, 1); return $content; } if ($char === $this->enclosure) { - return $content.$this->enclosure.$this->extractEnclosedFieldContent($line); + return $content.$this->enclosure.$this->extractEnclosedFieldContent(); } - return $content.$this->extractFieldContent($line); + return $content.$this->extractFieldContent(); } } From 33a8e517b5fd3cf9f532a027aa986338e8e542b2 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Mon, 24 Sep 2018 15:12:50 +0200 Subject: [PATCH 076/858] improve docblocks --- src/AbstractCsv.php | 1 - src/Stream.php | 3 --- src/XMLConverter.php | 3 --- 3 files changed, 7 deletions(-) diff --git a/src/AbstractCsv.php b/src/AbstractCsv.php index b1417bc1..6ca3a6b1 100644 --- a/src/AbstractCsv.php +++ b/src/AbstractCsv.php @@ -297,7 +297,6 @@ public function getContent(): string /** * Outputs all data on the CSV file. * - * * @return int Returns the number of characters read from the handle * and passed through to the output. */ diff --git a/src/Stream.php b/src/Stream.php index 1cbd314f..28adeede 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -210,7 +210,6 @@ public static function createFromPath(string $path, string $open_mode = 'r', $co /** * Return a new instance from a string. * - * * @return static */ public static function createFromString(string $content) @@ -256,7 +255,6 @@ public function setCsvControl(string $delimiter = ',', string $enclosure = '"', /** * Filter Csv control characters. * - * * @throws Exception If the Csv control character is not one character only. */ protected function filterControl(string $delimiter, string $enclosure, string $escape, string $caller): array @@ -464,7 +462,6 @@ public function fgets() * * @see http://php.net/manual/en/splfileobject.fseek.php * - * * @throws Exception if the stream resource is not seekable * * @return int diff --git a/src/XMLConverter.php b/src/XMLConverter.php index d061ae94..13157f68 100644 --- a/src/XMLConverter.php +++ b/src/XMLConverter.php @@ -109,7 +109,6 @@ public function convert($records): DOMDocument /** * Convert a CSV record into a DOMElement and * adds its offset as DOMElement attribute. - * */ protected function recordToElementWithAttribute( DOMDocument $doc, @@ -125,7 +124,6 @@ protected function recordToElementWithAttribute( /** * Convert a CSV record into a DOMElement. - * */ protected function recordToElement(DOMDocument $doc, array $record, string $field_encoder): DOMElement { @@ -181,7 +179,6 @@ public function rootElement(string $node_name): self /** * Filter XML element name. * - * * @throws DOMException If the Element name is invalid */ protected function filterElementName(string $value): string From 40dc42cc8c9977d1eaac01ea8713125d8370e6c9 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Mon, 24 Sep 2018 20:45:22 +0200 Subject: [PATCH 077/858] improve parser-fgetcsv compatibility --- src/RFC4180Parser.php | 20 ++++++++++++-------- tests/RFC4180ParserTest.php | 20 ++++++++++++++++---- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/RFC4180Parser.php b/src/RFC4180Parser.php index f8a22535..64088944 100644 --- a/src/RFC4180Parser.php +++ b/src/RFC4180Parser.php @@ -24,12 +24,12 @@ use function gettype; use function in_array; use function is_object; +use function ltrim; use function rtrim; use function sprintf; use function str_replace; use function strlen; use function substr; -use function trim; /** * A RFC4180 Compliant Parser in Pure PHP. @@ -132,19 +132,22 @@ public function getIterator() { $this->document->setFlags(0); $this->document->rewind(); - do { + while ($this->document->valid()) { $record = []; $this->line = $this->document->fgets(); do { $method = 'extractFieldContent'; - if (($this->line[0] ?? '') === $this->enclosure) { + $buffer = ltrim($this->line, $this->trim_mask); + if (($buffer[0] ?? '') === $this->enclosure) { $method = 'extractEnclosedFieldContent'; + $this->line = $buffer; } + $record[] = $this->$method(); } while (false !== $this->line); yield $record; - } while ($this->document->valid()); + } } /** @@ -165,10 +168,10 @@ private function extractFieldContent() list($content, $this->line) = explode($this->delimiter, $this->line, 2) + [1 => false]; if (false === $this->line) { - return trim(rtrim($content, "\r\n"), $this->trim_mask); + return rtrim($content, "\r\n"); } - return trim($content, $this->trim_mask); + return $content; } /** @@ -204,16 +207,17 @@ private function extractEnclosedFieldContent(): string } $char = $this->line[0] ?? ''; - if ($char === $this->delimiter) { + if ($this->delimiter === $char) { $this->line = substr($this->line, 1); return $content; } - if ($char === $this->enclosure) { + if ($this->enclosure === $char) { return $content.$this->enclosure.$this->extractEnclosedFieldContent(); } + return $content.$this->extractFieldContent(); } } diff --git a/tests/RFC4180ParserTest.php b/tests/RFC4180ParserTest.php index fa3ecab1..8079eee0 100644 --- a/tests/RFC4180ParserTest.php +++ b/tests/RFC4180ParserTest.php @@ -147,17 +147,17 @@ public function testKeepEmptyLines() * @covers ::extractFieldContent * @covers ::extractEnclosedFieldContent */ - public function testTrimSpaceWithNotEncloseField() + public function testNoTrimmedSpaceWithNotEncloseField() { $source = <<getIterator(), false); - self::assertSame(['Year', 'Make', 'Model', '', 'Description', 'Price'], $data[0]); - self::assertSame(['"1997', 'Ford', 'E350', 'ac, abs, moon', '3000.00'], $data[1]); + self::assertSame(['Year', 'Make', 'Model', '', 'Description', ' Price'], $data[0]); + self::assertSame(['1997', ' Ford ', 'E350 ', 'ac', ' abs', ' moon', ' 3000.00'], $data[1]); } /** @@ -236,4 +236,16 @@ public function testInvalidCsvParseAsFgetcsv() $parser_records = iterator_to_array($csv); self::assertEquals($fgetcsv_records, $parser_records); } + + public function testCsvParsedAsFgetcsv($value='') + { + $str = <<getIterator(), false); + self::assertEquals(['foo', 'foo bar', 'boo bar baz'], $records[0]); + self::assertEquals(['foo ', 'foo bar ', 'boo bar baz'], $records[1]); + } } From b54772bfec9b07f93ddcb8aab341fac2219b0bdb Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Tue, 25 Sep 2018 09:15:33 +0200 Subject: [PATCH 078/858] change parser name --- phpstan.tests.neon | 2 +- src/{RFC4180Parser.php => Parser.php} | 133 +++++++++--------- src/Reader.php | 2 +- .../{RFC4180ParserTest.php => ParserTest.php} | 68 ++++----- tests/WriterTest.php | 26 ++-- 5 files changed, 115 insertions(+), 116 deletions(-) rename src/{RFC4180Parser.php => Parser.php} (57%) rename tests/{RFC4180ParserTest.php => ParserTest.php} (80%) diff --git a/phpstan.tests.neon b/phpstan.tests.neon index 90cdb856..60b73569 100644 --- a/phpstan.tests.neon +++ b/phpstan.tests.neon @@ -10,5 +10,5 @@ parameters: - '#Parameter \#1 \$resource of class League\\Csv\\Stream constructor expects resource, string given.#' - '#Parameter \#1 \$records of method League\\Csv\\CharsetConverter::convert\(\) expects array|Traversable, string given.#' - '#Parameter \#2 \$delimiters of function League\\Csv\\delimiter_detect expects array, array given.#' - - '#Parameter \#1 \$document of class League\\Csv\\RFC4180Parser constructor expects League\\Csv\\Stream\|SplFileObject, array given.#' + - '#Parameter \#1 \$document of static method League\\Csv\\Parser::parse\(\) expects League\\Csv\\Stream\|SplFileObject, array given.#' reportUnmatchedIgnoredErrors: false diff --git a/src/RFC4180Parser.php b/src/Parser.php similarity index 57% rename from src/RFC4180Parser.php rename to src/Parser.php index 64088944..bd6e55f8 100644 --- a/src/RFC4180Parser.php +++ b/src/Parser.php @@ -16,7 +16,7 @@ namespace League\Csv; -use IteratorAggregate; +use Generator; use SplFileObject; use TypeError; use function explode; @@ -32,16 +32,16 @@ use function substr; /** - * A RFC4180 Compliant Parser in Pure PHP. + * A Polyfill to PHP's fgetcsv behavior with the empty string as the escape parameter. * * @see https://php.net/manual/en/function.fgetcsv.php * @see https://php.net/manual/en/function.fgets.php * @see https://tools.ietf.org/html/rfc4180 * @see http://edoceo.com/utilitas/csv-file-format * - * @internal used internally to produce RFC4180 compliant records + * @internal used internally to parse document without using the escape character */ -final class RFC4180Parser implements IteratorAggregate +final class Parser { /** * @internal @@ -51,39 +51,62 @@ final class RFC4180Parser implements IteratorAggregate /** * @var SplFileObject|Stream */ - private $document; + private static $document; /** * @var string */ - private $delimiter; + private static $delimiter; /** * @var string */ - private $enclosure; + private static $enclosure; /** * @var string */ - private $trim_mask; + private static $trim_mask; /** * @var string|bool */ - private $line = false; + private static $line; /** - * New instance. + * Converts the document into a CSV record iterator. + * + * The returned record array is similar to the returned value of fgetcsv + * + * - If the line is empty the record will be an array with a single value equals to null + * - Otherwise the array contains strings. * * @param SplFileObject|Stream $document */ - public function __construct($document, string $delimiter = ',', string $enclosure = '"') + public static function parse($document, string $delimiter = ',', string $enclosure = '"'): Generator { - $this->document = $this->filterDocument($document); - $this->delimiter = $this->filterControl($delimiter, 'delimiter'); - $this->enclosure = $this->filterControl($enclosure, 'enclosure'); - $this->trim_mask = str_replace([$this->delimiter, $this->enclosure], '', " \t\0\x0B"); + self::$document = self::filterDocument($document); + self::$delimiter = self::filterControl($delimiter, 'delimiter'); + self::$enclosure = self::filterControl($enclosure, 'enclosure'); + self::$trim_mask = str_replace([self::$delimiter, self::$enclosure], '', " \t\0\x0B"); + self::$document->setFlags(0); + self::$document->rewind(); + while (self::$document->valid()) { + $record = []; + self::$line = self::$document->fgets(); + do { + $method = 'extractFieldContent'; + $buffer = ltrim(self::$line, self::$trim_mask); + if (($buffer[0] ?? '') === self::$enclosure) { + $method = 'extractEnclosedFieldContent'; + self::$line = $buffer; + } + + $record[] = self::$method(); + } while (false !== self::$line); + + yield $record; + } } /** @@ -93,7 +116,7 @@ public function __construct($document, string $delimiter = ',', string $enclosur * * @return SplFileObject|Stream */ - private function filterDocument($document) + private static function filterDocument($document) { if ($document instanceof Stream || $document instanceof SplFileObject) { return $document; @@ -108,8 +131,10 @@ private function filterDocument($document) /** * Filter a control character. + * + * @throws Exception if the string is not a single byte character */ - private function filterControl(string $value, string $name): string + private static function filterControl(string $value, string $name): string { if (1 === strlen($value)) { return $value; @@ -118,38 +143,6 @@ private function filterControl(string $value, string $name): string throw new Exception(sprintf('Expected %s to be a single character %s given', $name, $value)); } - /** - * @inheritdoc - * - * Converts the stream into a CSV record iterator by extracting records one by one - * - * The returned record array is similar to the returned value of fgetcsv - * - * - If the line is empty the record will be an array with a single value equals to null - * - Otherwise the array contains strings. - */ - public function getIterator() - { - $this->document->setFlags(0); - $this->document->rewind(); - while ($this->document->valid()) { - $record = []; - $this->line = $this->document->fgets(); - do { - $method = 'extractFieldContent'; - $buffer = ltrim($this->line, $this->trim_mask); - if (($buffer[0] ?? '') === $this->enclosure) { - $method = 'extractEnclosedFieldContent'; - $this->line = $buffer; - } - - $record[] = $this->$method(); - } while (false !== $this->line); - - yield $record; - } - } - /** * Extract field without enclosure as per RFC4180. * @@ -158,16 +151,16 @@ public function getIterator() * * @return null|string */ - private function extractFieldContent() + private static function extractFieldContent() { - if (in_array($this->line, self::FIELD_BREAKS, true)) { - $this->line = false; + if (in_array(self::$line, self::FIELD_BREAKS, true)) { + self::$line = false; return null; } - list($content, $this->line) = explode($this->delimiter, $this->line, 2) + [1 => false]; - if (false === $this->line) { + list($content, self::$line) = explode(self::$delimiter, self::$line, 2) + [1 => false]; + if (false === self::$line) { return rtrim($content, "\r\n"); } @@ -181,43 +174,43 @@ private function extractFieldContent() * - Content inside enclosure must be preserved. * - Double enclosure sequence must be replaced by single enclosure character. * - Trailing line break must be removed if they are not part of the field content. - * - Invalid field do not throw as per fgetcsv behavior. + * - Invalid fields content are treated as per fgetcsv behavior. */ - private function extractEnclosedFieldContent(): string + private static function extractEnclosedFieldContent(): string { - if (($this->line[0] ?? '') === $this->enclosure) { - $this->line = substr($this->line, 1); + if ((self::$line[0] ?? '') === self::$enclosure) { + self::$line = substr(self::$line, 1); } $content = ''; - while (false !== $this->line) { - list($buffer, $remainder) = explode($this->enclosure, $this->line, 2) + [1 => false]; + while (false !== self::$line) { + list($buffer, $remainder) = explode(self::$enclosure, self::$line, 2) + [1 => false]; $content .= $buffer; if (false !== $remainder) { - $this->line = $remainder; + self::$line = $remainder; break; } - $this->line = $this->document->fgets(); + self::$line = self::$document->fgets(); } - if (in_array($this->line, self::FIELD_BREAKS, true)) { - $this->line = false; + if (in_array(self::$line, self::FIELD_BREAKS, true)) { + self::$line = false; return rtrim($content, "\r\n"); } - $char = $this->line[0] ?? ''; - if ($this->delimiter === $char) { - $this->line = substr($this->line, 1); + $char = self::$line[0] ?? ''; + if (self::$delimiter === $char) { + self::$line = substr(self::$line, 1); return $content; } - if ($this->enclosure === $char) { - return $content.$this->enclosure.$this->extractEnclosedFieldContent(); + if (self::$enclosure === $char) { + return $content.self::$enclosure.self::extractEnclosedFieldContent(); } - return $content.$this->extractFieldContent(); + return $content.self::extractFieldContent(); } } diff --git a/src/Reader.php b/src/Reader.php index 5f71e7f1..0de4b922 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -170,7 +170,7 @@ protected function seekRow(int $offset) protected function getDocument(): Iterator { if ('' === $this->escape && PHP_VERSION_ID < 70400) { - return (new RFC4180Parser($this->document, $this->delimiter, $this->enclosure))->getIterator(); + return Parser::parse($this->document, $this->delimiter, $this->enclosure); } $this->document->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY); diff --git a/tests/RFC4180ParserTest.php b/tests/ParserTest.php similarity index 80% rename from tests/RFC4180ParserTest.php rename to tests/ParserTest.php index 8079eee0..69c29358 100644 --- a/tests/RFC4180ParserTest.php +++ b/tests/ParserTest.php @@ -15,8 +15,8 @@ namespace LeagueTest\Csv; use League\Csv\Exception; +use League\Csv\Parser; use League\Csv\Reader; -use League\Csv\RFC4180Parser; use League\Csv\Stream; use PHPUnit\Framework\TestCase; use SplTempFileObject; @@ -25,45 +25,48 @@ /** * @group reader - * @coversDefaultClass League\Csv\RFC4180Parser + * @coversDefaultClass League\Csv\Parser */ -class RFC4180ParserTest extends TestCase +class ParserTest extends TestCase { /** - * @covers ::__construct + * @covers ::parse * @covers ::filterDocument */ public function testConstructorThrowsTypeErrorWithUnknownDocument() { self::expectException(TypeError::class); - new RFC4180Parser([]); + foreach (Parser::parse([]) as $record) { + } } /** - * @covers ::__construct + * @covers ::parse * @covers ::filterControl */ public function testConstructorThrowExceptionWithInvalidDelimiter() { self::expectException(Exception::class); - new RFC4180Parser(new SplTempFileObject(), 'toto'); + foreach (Parser::parse(new SplTempFileObject(), 'toto') as $record) { + } } /** - * @covers ::__construct + * @covers ::parse * @covers ::filterControl */ public function testConstructorThrowExceptionWithInvalidEnclosure() { self::expectException(Exception::class); - new RFC4180Parser(new SplTempFileObject(), ';', 'é'); + foreach (Parser::parse(new SplTempFileObject(), ',', 'é') as $record) { + } } /** * @covers \League\Csv\Stream::fgets - * @covers ::__construct + * @covers ::parse * @covers ::filterDocument - * @covers ::getIterator + * @covers ::parse * @covers ::extractFieldContent * @covers ::extractEnclosedFieldContent */ @@ -82,15 +85,15 @@ public function testWorksWithMultiLines() MUST SELL! air, moon roof, loaded EOF; - $iterator = new RFC4180Parser(Stream::createFromString($source)); - self::assertCount(5, $iterator); - $data = iterator_to_array($iterator->getIterator(), false); + $iterator = Parser::parse(Stream::createFromString($source)); + $data = iterator_to_array($iterator, false); + self::assertCount(5, $data); self::assertSame($multiline, $data[4][3]); } /** * @covers \League\Csv\Stream::fgets - * @covers ::getIterator + * @covers ::parse * @covers ::extractFieldContent * @covers ::extractEnclosedFieldContent */ @@ -110,14 +113,13 @@ public function testWorksWithMultiLinesWithDifferentDelimiter() air| moon roof| loaded EOF; $doc = Stream::createFromString($source); - $iterator = new RFC4180Parser($doc, '|', "'"); - self::assertCount(5, $iterator); - $data = iterator_to_array($iterator->getIterator(), false); + $data = iterator_to_array(Parser::parse($doc, '|', "'"), false); + self::assertCount(5, $data); self::assertSame($multiline, $data[4][3]); } /** - * @covers ::getIterator + * @covers ::parse * @covers ::extractFieldContent * @covers ::extractEnclosedFieldContent */ @@ -132,10 +134,9 @@ public function testKeepEmptyLines() $rsrc = new SplTempFileObject(); $rsrc->fwrite($source); - $iterator = new RFC4180Parser($rsrc); - self::assertCount(4, $iterator); - $data = iterator_to_array($iterator->getIterator(), false); + $data = iterator_to_array(Parser::parse($rsrc), false); + self::assertCount(4, $data); self::assertSame(['parent name', 'child name', 'title'], $data[0]); self::assertSame([0 => null], $data[1]); self::assertSame([0 => null], $data[2]); @@ -143,7 +144,7 @@ public function testKeepEmptyLines() } /** - * @covers ::getIterator + * @covers ::parse * @covers ::extractFieldContent * @covers ::extractEnclosedFieldContent */ @@ -153,15 +154,14 @@ public function testNoTrimmedSpaceWithNotEncloseField() Year,Make,Model,,Description, Price 1997, Ford ,E350 ,ac, abs, moon, 3000.00 EOF; - $iterator = new RFC4180Parser(Stream::createFromString($source)); - self::assertCount(2, $iterator); - $data = iterator_to_array($iterator->getIterator(), false); + $data = iterator_to_array(Parser::parse(Stream::createFromString($source)), false); + self::assertCount(2, $data); self::assertSame(['Year', 'Make', 'Model', '', 'Description', ' Price'], $data[0]); self::assertSame(['1997', ' Ford ', 'E350 ', 'ac', ' abs', ' moon', ' 3000.00'], $data[1]); } /** - * @covers ::getIterator + * @covers ::parse * @covers ::extractFieldContent * @covers ::extractEnclosedFieldContent * @@ -169,8 +169,8 @@ public function testNoTrimmedSpaceWithNotEncloseField() */ public function testHandlingInvalidCSVwithEnclosure(string $string, array $record) { - $iterator = new RFC4180Parser(Stream::createFromString($string)); - $data = iterator_to_array($iterator->getIterator(), false); + $iterator = Parser::parse(Stream::createFromString($string)); + $data = iterator_to_array($iterator, false); self::assertSame($record, $data[0]); } @@ -197,7 +197,7 @@ public function invalidCsvRecordProvider() } /** - * @covers ::getIterator + * @covers ::parse * @covers ::extractFieldContent * @covers ::extractEnclosedFieldContent */ @@ -218,12 +218,12 @@ public function testDoubleEnclosure() ]; $stream = Stream::createFromString($str); - $records = new RFC4180Parser($stream, ';'); - self::assertEquals($expected, iterator_to_array($records->getIterator(), false)); + $records = Parser::parse($stream, ';'); + self::assertEquals($expected, iterator_to_array($records, false)); } /** - * @covers ::getIterator + * @covers ::parse * @covers ::extractFieldContent * @covers ::extractEnclosedFieldContent */ @@ -244,7 +244,7 @@ public function testCsvParsedAsFgetcsv($value='') "foo" , "foo bar" , "boo bar baz" EOF; $stream = Stream::createFromString($str); - $records = iterator_to_array((new RFC4180Parser($stream))->getIterator(), false); + $records = iterator_to_array((Parser::parse($stream)), false); self::assertEquals(['foo', 'foo bar', 'boo bar baz'], $records[0]); self::assertEquals(['foo ', 'foo bar ', 'boo bar baz'], $records[1]); } diff --git a/tests/WriterTest.php b/tests/WriterTest.php index 8068ed35..9e6ef05a 100644 --- a/tests/WriterTest.php +++ b/tests/WriterTest.php @@ -261,7 +261,7 @@ public function testWriterTriggerExceptionWithNonSeekableStream() */ public function testRFC4180WriterMode(string $expected, array $record) { - $csv = Writer::createFromPath('php://temp'); + $csv = Writer::createFromString(); $csv->setNewline("\r\n"); $csv->setEscape(''); $csv->insertOne($record); @@ -270,43 +270,49 @@ public function testRFC4180WriterMode(string $expected, array $record) public function compliantRFC4180Provider() { + $eol = "\r\n"; + return [ 'bug #43225' => [ - 'expected' => '"a\""",bbb'."\r\n", + 'expected' => '"a\""",bbb'.$eol, 'record' => ['a\\"', 'bbb'], ], 'bug #74713' => [ - 'expected' => '"""@@"",""B"""'."\r\n", + 'expected' => '"""@@"",""B"""'.$eol, 'record' => ['"@@","B"'], ], 'bug #55413' => [ - 'expected' => 'A,"Some ""Stuff""",C'."\r\n", + 'expected' => 'A,"Some ""Stuff""",C'.$eol, 'record' => ['A', 'Some "Stuff"', 'C'], ], 'convert boolean' => [ - 'expected' => ',"Some ""Stuff""",C'."\r\n", + 'expected' => ',"Some ""Stuff""",C'.$eol, 'record' => [false, 'Some "Stuff"', 'C'], ], 'convert null value' => [ - 'expected' => ',"Some ""Stuff""",C'."\r\n", + 'expected' => ',"Some ""Stuff""",C'.$eol, 'record' => [null, 'Some "Stuff"', 'C'], ], 'bug #307' => [ - 'expected' => '"a text string \\",...'."\r\n", + 'expected' => '"a text string \\",...'.$eol, 'record' => ['a text string \\', '...'], ], 'line starting with space' => [ - 'expected' => '" a",foo,bar'."\r\n", + 'expected' => '" a",foo,bar'.$eol, 'record' => [' a', 'foo', 'bar'], ], 'line ending with space' => [ - 'expected' => 'a,foo,"bar "'."\r\n", + 'expected' => 'a,foo,"bar "'.$eol, 'record' => ['a', 'foo', 'bar '], ], 'line containing space' => [ - 'expected' => 'a,"foo bar",bar'."\r\n", + 'expected' => 'a,"foo bar",bar'.$eol, 'record' => ['a', 'foo bar', 'bar'], ], + 'multiline' => [ + 'expected' => "a,\"foo bar\",\"multiline\r\nfield\",bar$eol", + 'record' => ['a', 'foo bar', "multiline\r\nfield", 'bar'], + ], ]; } } From 5257f0f531f3341966f34b6318cbd06709f11d94 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Tue, 25 Sep 2018 09:25:38 +0200 Subject: [PATCH 079/858] improve writer testsuite with empty escape char --- tests/WriterTest.php | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/WriterTest.php b/tests/WriterTest.php index 9e6ef05a..315aa858 100644 --- a/tests/WriterTest.php +++ b/tests/WriterTest.php @@ -261,56 +261,56 @@ public function testWriterTriggerExceptionWithNonSeekableStream() */ public function testRFC4180WriterMode(string $expected, array $record) { - $csv = Writer::createFromString(); - $csv->setNewline("\r\n"); - $csv->setEscape(''); - $csv->insertOne($record); - self::assertSame($expected, $csv->getContent()); + foreach (["\r\n", "\n", "\r"] as $eol) { + $csv = Writer::createFromString(); + $csv->setNewline($eol); + $csv->setEscape(''); + $csv->insertOne($record); + self::assertSame($expected.$eol, $csv->getContent()); + } } public function compliantRFC4180Provider() { - $eol = "\r\n"; - return [ 'bug #43225' => [ - 'expected' => '"a\""",bbb'.$eol, + 'expected' => '"a\""",bbb', 'record' => ['a\\"', 'bbb'], ], 'bug #74713' => [ - 'expected' => '"""@@"",""B"""'.$eol, + 'expected' => '"""@@"",""B"""', 'record' => ['"@@","B"'], ], 'bug #55413' => [ - 'expected' => 'A,"Some ""Stuff""",C'.$eol, + 'expected' => 'A,"Some ""Stuff""",C', 'record' => ['A', 'Some "Stuff"', 'C'], ], 'convert boolean' => [ - 'expected' => ',"Some ""Stuff""",C'.$eol, + 'expected' => ',"Some ""Stuff""",C', 'record' => [false, 'Some "Stuff"', 'C'], ], 'convert null value' => [ - 'expected' => ',"Some ""Stuff""",C'.$eol, + 'expected' => ',"Some ""Stuff""",C', 'record' => [null, 'Some "Stuff"', 'C'], ], 'bug #307' => [ - 'expected' => '"a text string \\",...'.$eol, + 'expected' => '"a text string \\",...', 'record' => ['a text string \\', '...'], ], 'line starting with space' => [ - 'expected' => '" a",foo,bar'.$eol, + 'expected' => '" a",foo,bar', 'record' => [' a', 'foo', 'bar'], ], 'line ending with space' => [ - 'expected' => 'a,foo,"bar "'.$eol, + 'expected' => 'a,foo,"bar "', 'record' => ['a', 'foo', 'bar '], ], 'line containing space' => [ - 'expected' => 'a,"foo bar",bar'.$eol, + 'expected' => 'a,"foo bar",bar', 'record' => ['a', 'foo bar', 'bar'], ], 'multiline' => [ - 'expected' => "a,\"foo bar\",\"multiline\r\nfield\",bar$eol", + 'expected' => "a,\"foo bar\",\"multiline\r\nfield\",bar", 'record' => ['a', 'foo bar', "multiline\r\nfield", 'bar'], ], ]; From b4a8120ca1aa3158575813201f8249ee415896b7 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Tue, 25 Sep 2018 22:28:29 +0200 Subject: [PATCH 080/858] rename Empty Escape Character Parser --- phpstan.tests.neon | 2 +- src/{Parser.php => EmptyEscapeParser.php} | 3 +- src/Reader.php | 2 +- ...rserTest.php => EmptyEscapeParserTest.php} | 75 ++++++++++++------- 4 files changed, 49 insertions(+), 33 deletions(-) rename src/{Parser.php => EmptyEscapeParser.php} (99%) rename tests/{ParserTest.php => EmptyEscapeParserTest.php} (75%) diff --git a/phpstan.tests.neon b/phpstan.tests.neon index 60b73569..9ee28f9b 100644 --- a/phpstan.tests.neon +++ b/phpstan.tests.neon @@ -10,5 +10,5 @@ parameters: - '#Parameter \#1 \$resource of class League\\Csv\\Stream constructor expects resource, string given.#' - '#Parameter \#1 \$records of method League\\Csv\\CharsetConverter::convert\(\) expects array|Traversable, string given.#' - '#Parameter \#2 \$delimiters of function League\\Csv\\delimiter_detect expects array, array given.#' - - '#Parameter \#1 \$document of static method League\\Csv\\Parser::parse\(\) expects League\\Csv\\Stream\|SplFileObject, array given.#' + - '#Parameter \#1 \$document of static method League\\Csv\\EmptyEscapeParser::parse\(\) expects League\\Csv\\Stream\|SplFileObject, array given.#' reportUnmatchedIgnoredErrors: false diff --git a/src/Parser.php b/src/EmptyEscapeParser.php similarity index 99% rename from src/Parser.php rename to src/EmptyEscapeParser.php index bd6e55f8..40eed697 100644 --- a/src/Parser.php +++ b/src/EmptyEscapeParser.php @@ -41,7 +41,7 @@ * * @internal used internally to parse document without using the escape character */ -final class Parser +final class EmptyEscapeParser { /** * @internal @@ -210,7 +210,6 @@ private static function extractEnclosedFieldContent(): string return $content.self::$enclosure.self::extractEnclosedFieldContent(); } - return $content.self::extractFieldContent(); } } diff --git a/src/Reader.php b/src/Reader.php index 0de4b922..5cad387a 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -170,7 +170,7 @@ protected function seekRow(int $offset) protected function getDocument(): Iterator { if ('' === $this->escape && PHP_VERSION_ID < 70400) { - return Parser::parse($this->document, $this->delimiter, $this->enclosure); + return EmptyEscapeParser::parse($this->document, $this->delimiter, $this->enclosure); } $this->document->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY); diff --git a/tests/ParserTest.php b/tests/EmptyEscapeParserTest.php similarity index 75% rename from tests/ParserTest.php rename to tests/EmptyEscapeParserTest.php index 69c29358..48908ad0 100644 --- a/tests/ParserTest.php +++ b/tests/EmptyEscapeParserTest.php @@ -14,8 +14,8 @@ namespace LeagueTest\Csv; +use League\Csv\EmptyEscapeParser; use League\Csv\Exception; -use League\Csv\Parser; use League\Csv\Reader; use League\Csv\Stream; use PHPUnit\Framework\TestCase; @@ -25,9 +25,9 @@ /** * @group reader - * @coversDefaultClass League\Csv\Parser + * @coversDefaultClass League\Csv\EmptyEscapeParser */ -class ParserTest extends TestCase +class EmptyEscapeParserTest extends TestCase { /** * @covers ::parse @@ -36,7 +36,7 @@ class ParserTest extends TestCase public function testConstructorThrowsTypeErrorWithUnknownDocument() { self::expectException(TypeError::class); - foreach (Parser::parse([]) as $record) { + foreach (EmptyEscapeParser::parse([]) as $record) { } } @@ -47,7 +47,7 @@ public function testConstructorThrowsTypeErrorWithUnknownDocument() public function testConstructorThrowExceptionWithInvalidDelimiter() { self::expectException(Exception::class); - foreach (Parser::parse(new SplTempFileObject(), 'toto') as $record) { + foreach (EmptyEscapeParser::parse(new SplTempFileObject(), 'toto') as $record) { } } @@ -58,7 +58,7 @@ public function testConstructorThrowExceptionWithInvalidDelimiter() public function testConstructorThrowExceptionWithInvalidEnclosure() { self::expectException(Exception::class); - foreach (Parser::parse(new SplTempFileObject(), ',', 'é') as $record) { + foreach (EmptyEscapeParser::parse(new SplTempFileObject(), ',', 'é') as $record) { } } @@ -85,7 +85,7 @@ public function testWorksWithMultiLines() MUST SELL! air, moon roof, loaded EOF; - $iterator = Parser::parse(Stream::createFromString($source)); + $iterator = EmptyEscapeParser::parse(Stream::createFromString($source)); $data = iterator_to_array($iterator, false); self::assertCount(5, $data); self::assertSame($multiline, $data[4][3]); @@ -113,7 +113,7 @@ public function testWorksWithMultiLinesWithDifferentDelimiter() air| moon roof| loaded EOF; $doc = Stream::createFromString($source); - $data = iterator_to_array(Parser::parse($doc, '|', "'"), false); + $data = iterator_to_array(EmptyEscapeParser::parse($doc, '|', "'"), false); self::assertCount(5, $data); self::assertSame($multiline, $data[4][3]); } @@ -135,12 +135,16 @@ public function testKeepEmptyLines() $rsrc = new SplTempFileObject(); $rsrc->fwrite($source); - $data = iterator_to_array(Parser::parse($rsrc), false); - self::assertCount(4, $data); - self::assertSame(['parent name', 'child name', 'title'], $data[0]); - self::assertSame([0 => null], $data[1]); - self::assertSame([0 => null], $data[2]); - self::assertSame(['parentA', 'childA', 'titleA'], $data[3]); + $expected = [ + ['parent name', 'child name', 'title'], + [null], + [null], + ['parentA', 'childA', 'titleA'], + ]; + + foreach (EmptyEscapeParser::parse($rsrc) as $offset => $record) { + self::assertSame($expected[$offset], $record); + } } /** @@ -154,10 +158,16 @@ public function testNoTrimmedSpaceWithNotEncloseField() Year,Make,Model,,Description, Price 1997, Ford ,E350 ,ac, abs, moon, 3000.00 EOF; - $data = iterator_to_array(Parser::parse(Stream::createFromString($source)), false); - self::assertCount(2, $data); - self::assertSame(['Year', 'Make', 'Model', '', 'Description', ' Price'], $data[0]); - self::assertSame(['1997', ' Ford ', 'E350 ', 'ac', ' abs', ' moon', ' 3000.00'], $data[1]); + + $expected = [ + ['Year', 'Make', 'Model', '', 'Description', ' Price'], + ['1997', ' Ford ', 'E350 ', 'ac', ' abs', ' moon', ' 3000.00'], + ]; + + $stream = Stream::createFromString($source); + foreach (EmptyEscapeParser::parse($stream) as $offset => $record) { + self::assertSame($expected[$offset], $record); + } } /** @@ -167,11 +177,12 @@ public function testNoTrimmedSpaceWithNotEncloseField() * * @dataProvider invalidCsvRecordProvider */ - public function testHandlingInvalidCSVwithEnclosure(string $string, array $record) + public function testHandlingInvalidCSVwithEnclosure(string $string, array $expected) { - $iterator = Parser::parse(Stream::createFromString($string)); - $data = iterator_to_array($iterator, false); - self::assertSame($record, $data[0]); + $stream = Stream::createFromString($string); + foreach (EmptyEscapeParser::parse($stream) as $record) { + self::assertSame($expected, $record); + } } public function invalidCsvRecordProvider() @@ -218,8 +229,10 @@ public function testDoubleEnclosure() ]; $stream = Stream::createFromString($str); - $records = Parser::parse($stream, ';'); - self::assertEquals($expected, iterator_to_array($records, false)); + $records = EmptyEscapeParser::parse($stream, ';'); + foreach ($records as $offset => $record) { + self::assertSame($expected[$offset], $record); + } } /** @@ -233,8 +246,7 @@ public function testInvalidCsvParseAsFgetcsv() $csv = Reader::createFromString($str); $fgetcsv_records = iterator_to_array($csv); $csv->setEscape(''); - $parser_records = iterator_to_array($csv); - self::assertEquals($fgetcsv_records, $parser_records); + self::assertEquals($fgetcsv_records, iterator_to_array($csv)); } public function testCsvParsedAsFgetcsv($value='') @@ -243,9 +255,14 @@ public function testCsvParsedAsFgetcsv($value='') "foo","foo bar","boo bar baz" "foo" , "foo bar" , "boo bar baz" EOF; + $expected = [ + ['foo', 'foo bar', 'boo bar baz'], + ['foo ', 'foo bar ', 'boo bar baz'], + ]; + $stream = Stream::createFromString($str); - $records = iterator_to_array((Parser::parse($stream)), false); - self::assertEquals(['foo', 'foo bar', 'boo bar baz'], $records[0]); - self::assertEquals(['foo ', 'foo bar ', 'boo bar baz'], $records[1]); + foreach (EmptyEscapeParser::parse($stream) as $offset => $record) { + self::assertEquals($expected[$offset], $record); + } } } From 4012015b45e4fa45c72808f9ed2f4a7f4fe44bd8 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Tue, 25 Sep 2018 22:41:33 +0200 Subject: [PATCH 081/858] Bug fix Reader::getHeader method --- CHANGELOG.md | 1 + src/Reader.php | 2 +- tests/ReaderTest.php | 23 +++++++++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa392e76..01e99fa3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ All Notable changes to `Csv` will be documented in this file - `XMLConverter` convert to string the record value to avoid PHP warning on `null` value - Internal `Stream::fwrite` improved - Internal `Stream::__destruct` no longer emit warning on invalid stream filter removal. +- `Reader:getHeader` when the record is an empty line. ### Removed diff --git a/src/Reader.php b/src/Reader.php index 5cad387a..f64727bc 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -157,7 +157,7 @@ protected function seekRow(int $offset) { foreach ($this->getDocument() as $index => $record) { if ($offset === $index) { - return $record; + return [null] === $record ? false : $record; } } diff --git a/tests/ReaderTest.php b/tests/ReaderTest.php index 99fc33f7..d28379c7 100644 --- a/tests/ReaderTest.php +++ b/tests/ReaderTest.php @@ -228,6 +228,29 @@ public function testHeaderThrowsExceptionOnError() iterator_to_array($csv); } + + /** + * @covers ::getHeader + * @covers ::seekRow + * @covers ::setHeaderOffset + * @covers League\Csv\Exception + */ + public function testHeaderThrowsExceptionOnEmptyLine() + { + self::expectException(Exception::class); + $str = <<setHeaderOffset(2); + $csv->getHeader(); + } + + + /** * @covers ::stripBOM * @covers ::removeBOM From 7506b870fb2b3b9ffd58c177533b8f16fb30f8b7 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Wed, 26 Sep 2018 12:37:39 +0200 Subject: [PATCH 082/858] adding the Polyfill namespace --- phpstan.tests.neon | 2 +- src/{ => Polyfill}/EmptyEscapeParser.php | 4 +++- src/Reader.php | 1 + tests/{ => Polyfill}/EmptyEscapeParserTest.php | 6 +++--- 4 files changed, 8 insertions(+), 5 deletions(-) rename src/{ => Polyfill}/EmptyEscapeParser.php (98%) rename tests/{ => Polyfill}/EmptyEscapeParserTest.php (98%) diff --git a/phpstan.tests.neon b/phpstan.tests.neon index 9ee28f9b..457d830e 100644 --- a/phpstan.tests.neon +++ b/phpstan.tests.neon @@ -10,5 +10,5 @@ parameters: - '#Parameter \#1 \$resource of class League\\Csv\\Stream constructor expects resource, string given.#' - '#Parameter \#1 \$records of method League\\Csv\\CharsetConverter::convert\(\) expects array|Traversable, string given.#' - '#Parameter \#2 \$delimiters of function League\\Csv\\delimiter_detect expects array, array given.#' - - '#Parameter \#1 \$document of static method League\\Csv\\EmptyEscapeParser::parse\(\) expects League\\Csv\\Stream\|SplFileObject, array given.#' + - '#Parameter \#1 \$document of static method League\\Csv\\Polyfill\\EmptyEscapeParser::parse\(\) expects League\\Csv\\Stream\|SplFileObject, array given.#' reportUnmatchedIgnoredErrors: false diff --git a/src/EmptyEscapeParser.php b/src/Polyfill/EmptyEscapeParser.php similarity index 98% rename from src/EmptyEscapeParser.php rename to src/Polyfill/EmptyEscapeParser.php index 40eed697..4d99d0e5 100644 --- a/src/EmptyEscapeParser.php +++ b/src/Polyfill/EmptyEscapeParser.php @@ -14,9 +14,11 @@ declare(strict_types=1); -namespace League\Csv; +namespace League\Csv\Polyfill; use Generator; +use League\Csv\Exception; +use League\Csv\Stream; use SplFileObject; use TypeError; use function explode; diff --git a/src/Reader.php b/src/Reader.php index f64727bc..6777fde2 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -22,6 +22,7 @@ use Iterator; use IteratorAggregate; use JsonSerializable; +use League\Csv\Polyfill\EmptyEscapeParser; use SplFileObject; use TypeError; use const STREAM_FILTER_READ; diff --git a/tests/EmptyEscapeParserTest.php b/tests/Polyfill/EmptyEscapeParserTest.php similarity index 98% rename from tests/EmptyEscapeParserTest.php rename to tests/Polyfill/EmptyEscapeParserTest.php index 48908ad0..d84fff13 100644 --- a/tests/EmptyEscapeParserTest.php +++ b/tests/Polyfill/EmptyEscapeParserTest.php @@ -12,10 +12,10 @@ * file that was distributed with this source code. */ -namespace LeagueTest\Csv; +namespace LeagueTest\Csv\Polyfill; -use League\Csv\EmptyEscapeParser; use League\Csv\Exception; +use League\Csv\Polyfill\EmptyEscapeParser; use League\Csv\Reader; use League\Csv\Stream; use PHPUnit\Framework\TestCase; @@ -25,7 +25,7 @@ /** * @group reader - * @coversDefaultClass League\Csv\EmptyEscapeParser + * @coversDefaultClass League\Csv\Polyfill\EmptyEscapeParser */ class EmptyEscapeParserTest extends TestCase { From 20b3b0441e677393b4edf24c009a2580399327bb Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Wed, 26 Sep 2018 21:04:08 +0200 Subject: [PATCH 083/858] Improve PolyFill parser --- src/Polyfill/EmptyEscapeParser.php | 4 +++- src/Reader.php | 2 +- tests/Polyfill/EmptyEscapeParserTest.php | 2 -- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Polyfill/EmptyEscapeParser.php b/src/Polyfill/EmptyEscapeParser.php index 4d99d0e5..d0c7c322 100644 --- a/src/Polyfill/EmptyEscapeParser.php +++ b/src/Polyfill/EmptyEscapeParser.php @@ -107,7 +107,9 @@ public static function parse($document, string $delimiter = ',', string $enclosu $record[] = self::$method(); } while (false !== self::$line); - yield $record; + if ([null] !== $record) { + yield $record; + } } } diff --git a/src/Reader.php b/src/Reader.php index 6777fde2..2396193d 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -158,7 +158,7 @@ protected function seekRow(int $offset) { foreach ($this->getDocument() as $index => $record) { if ($offset === $index) { - return [null] === $record ? false : $record; + return $record; } } diff --git a/tests/Polyfill/EmptyEscapeParserTest.php b/tests/Polyfill/EmptyEscapeParserTest.php index d84fff13..ae5780bc 100644 --- a/tests/Polyfill/EmptyEscapeParserTest.php +++ b/tests/Polyfill/EmptyEscapeParserTest.php @@ -137,8 +137,6 @@ public function testKeepEmptyLines() $expected = [ ['parent name', 'child name', 'title'], - [null], - [null], ['parentA', 'childA', 'titleA'], ]; From e483904c419e47db3ebecf37fb2f9b9e1ae4d469 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Wed, 26 Sep 2018 21:15:55 +0200 Subject: [PATCH 084/858] Improve docblock --- src/Polyfill/EmptyEscapeParser.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Polyfill/EmptyEscapeParser.php b/src/Polyfill/EmptyEscapeParser.php index d0c7c322..dddaaa61 100644 --- a/src/Polyfill/EmptyEscapeParser.php +++ b/src/Polyfill/EmptyEscapeParser.php @@ -34,14 +34,16 @@ use function substr; /** - * A Polyfill to PHP's fgetcsv behavior with the empty string as the escape parameter. + * A Polyfill to PHP's SplFileObject behavior when reading a CSV document + * with the SplFileObject::READ_CSV and SplFileObject::SKIP_EMPTY flags on + * and the empty string as the escape parameter. * * @see https://php.net/manual/en/function.fgetcsv.php * @see https://php.net/manual/en/function.fgets.php * @see https://tools.ietf.org/html/rfc4180 * @see http://edoceo.com/utilitas/csv-file-format * - * @internal used internally to parse document without using the escape character + * @internal used internally to parse a CSV document without using the escape character */ final class EmptyEscapeParser { @@ -80,7 +82,7 @@ final class EmptyEscapeParser * * The returned record array is similar to the returned value of fgetcsv * - * - If the line is empty the record will be an array with a single value equals to null + * - If the line is empty the record is skipped * - Otherwise the array contains strings. * * @param SplFileObject|Stream $document From 3dc219ad196070abbf7287bbbfebd83e1c20e9e0 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Thu, 27 Sep 2018 08:44:45 +0200 Subject: [PATCH 085/858] simplify polyfill parser --- src/Polyfill/EmptyEscapeParser.php | 21 ++--------------- src/Reader.php | 6 +++-- tests/Polyfill/EmptyEscapeParserTest.php | 29 ++++-------------------- 3 files changed, 10 insertions(+), 46 deletions(-) diff --git a/src/Polyfill/EmptyEscapeParser.php b/src/Polyfill/EmptyEscapeParser.php index 4d99d0e5..f2079e54 100644 --- a/src/Polyfill/EmptyEscapeParser.php +++ b/src/Polyfill/EmptyEscapeParser.php @@ -17,7 +17,6 @@ namespace League\Csv\Polyfill; use Generator; -use League\Csv\Exception; use League\Csv\Stream; use SplFileObject; use TypeError; @@ -30,7 +29,6 @@ use function rtrim; use function sprintf; use function str_replace; -use function strlen; use function substr; /** @@ -85,11 +83,10 @@ final class EmptyEscapeParser * * @param SplFileObject|Stream $document */ - public static function parse($document, string $delimiter = ',', string $enclosure = '"'): Generator + public static function parse($document): Generator { self::$document = self::filterDocument($document); - self::$delimiter = self::filterControl($delimiter, 'delimiter'); - self::$enclosure = self::filterControl($enclosure, 'enclosure'); + list(self::$delimiter, self::$enclosure, ) = self::$document->getCsvControl(); self::$trim_mask = str_replace([self::$delimiter, self::$enclosure], '', " \t\0\x0B"); self::$document->setFlags(0); self::$document->rewind(); @@ -131,20 +128,6 @@ private static function filterDocument($document) )); } - /** - * Filter a control character. - * - * @throws Exception if the string is not a single byte character - */ - private static function filterControl(string $value, string $name): string - { - if (1 === strlen($value)) { - return $value; - } - - throw new Exception(sprintf('Expected %s to be a single character %s given', $name, $value)); - } - /** * Extract field without enclosure as per RFC4180. * diff --git a/src/Reader.php b/src/Reader.php index 6777fde2..49ad0620 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -170,8 +170,10 @@ protected function seekRow(int $offset) */ protected function getDocument(): Iterator { - if ('' === $this->escape && PHP_VERSION_ID < 70400) { - return EmptyEscapeParser::parse($this->document, $this->delimiter, $this->enclosure); + if ('' === $this->escape) { + $this->document->setCsvControl($this->delimiter, $this->enclosure); + + return EmptyEscapeParser::parse($this->document); } $this->document->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY); diff --git a/tests/Polyfill/EmptyEscapeParserTest.php b/tests/Polyfill/EmptyEscapeParserTest.php index d84fff13..df2dcc12 100644 --- a/tests/Polyfill/EmptyEscapeParserTest.php +++ b/tests/Polyfill/EmptyEscapeParserTest.php @@ -14,7 +14,6 @@ namespace LeagueTest\Csv\Polyfill; -use League\Csv\Exception; use League\Csv\Polyfill\EmptyEscapeParser; use League\Csv\Reader; use League\Csv\Stream; @@ -40,28 +39,6 @@ public function testConstructorThrowsTypeErrorWithUnknownDocument() } } - /** - * @covers ::parse - * @covers ::filterControl - */ - public function testConstructorThrowExceptionWithInvalidDelimiter() - { - self::expectException(Exception::class); - foreach (EmptyEscapeParser::parse(new SplTempFileObject(), 'toto') as $record) { - } - } - - /** - * @covers ::parse - * @covers ::filterControl - */ - public function testConstructorThrowExceptionWithInvalidEnclosure() - { - self::expectException(Exception::class); - foreach (EmptyEscapeParser::parse(new SplTempFileObject(), ',', 'é') as $record) { - } - } - /** * @covers \League\Csv\Stream::fgets * @covers ::parse @@ -113,7 +90,8 @@ public function testWorksWithMultiLinesWithDifferentDelimiter() air| moon roof| loaded EOF; $doc = Stream::createFromString($source); - $data = iterator_to_array(EmptyEscapeParser::parse($doc, '|', "'"), false); + $doc->setCsvControl('|', "'"); + $data = iterator_to_array(EmptyEscapeParser::parse($doc), false); self::assertCount(5, $data); self::assertSame($multiline, $data[4][3]); } @@ -229,7 +207,8 @@ public function testDoubleEnclosure() ]; $stream = Stream::createFromString($str); - $records = EmptyEscapeParser::parse($stream, ';'); + $stream->setCsvControl(';', '"'); + $records = EmptyEscapeParser::parse($stream); foreach ($records as $offset => $record) { self::assertSame($expected[$offset], $record); } From 27a0dd8b9fd1e43ee5b44c486c114c88353fad47 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Thu, 27 Sep 2018 09:14:27 +0200 Subject: [PATCH 086/858] Improve Polyfill parser --- src/Polyfill/EmptyEscapeParser.php | 65 ++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 21 deletions(-) diff --git a/src/Polyfill/EmptyEscapeParser.php b/src/Polyfill/EmptyEscapeParser.php index 2b41887d..5ba2354d 100644 --- a/src/Polyfill/EmptyEscapeParser.php +++ b/src/Polyfill/EmptyEscapeParser.php @@ -36,6 +36,21 @@ * with the SplFileObject::READ_CSV and SplFileObject::SKIP_EMPTY flags on * and the empty string as the escape parameter. * + * + * $file = new SplFileObject('/path/to/file.csv', 'r'); + * $file->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY); + * $file->setCsvControl($delimiter, $enclosure, ''); //this does not currently in any PHP stable release + * + * + * instead you can do this + * + * + * $file = new SplFileObject('/path/to/file.csv', 'r'); + * $file->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY); + * $file->setCsvControl($delimiter, $enclosure, $escape); + * EmptyEscapeParser::parse($file); //parsing will be done while ignoring the escape character value. + * + * * @see https://php.net/manual/en/function.fgetcsv.php * @see https://php.net/manual/en/function.fgets.php * @see https://tools.ietf.org/html/rfc4180 @@ -78,12 +93,11 @@ final class EmptyEscapeParser /** * Converts the document into a CSV record iterator. * - * The returned record array is similar to the returned value of fgetcsv - * - * - If the line is empty the record is skipped - * - Otherwise the array contains strings. + * Each record array contains strings elements. * * @param SplFileObject|Stream $document + * + * @return Generator|array[] */ public static function parse($document): Generator { @@ -93,19 +107,7 @@ public static function parse($document): Generator self::$document->setFlags(0); self::$document->rewind(); while (self::$document->valid()) { - $record = []; - self::$line = self::$document->fgets(); - do { - $method = 'extractFieldContent'; - $buffer = ltrim(self::$line, self::$trim_mask); - if (($buffer[0] ?? '') === self::$enclosure) { - $method = 'extractEnclosedFieldContent'; - self::$line = $buffer; - } - - $record[] = self::$method(); - } while (false !== self::$line); - + $record = self::extractRecord(); if ([null] !== $record) { yield $record; } @@ -113,7 +115,7 @@ public static function parse($document): Generator } /** - * Filter the submitted document. + * Filters the submitted document. * * @param SplFileObject|Stream $document * @@ -133,7 +135,28 @@ private static function filterDocument($document) } /** - * Extract field without enclosure as per RFC4180. + * Extracts a record form the CSV document. + */ + private static function extractRecord(): array + { + $record = []; + self::$line = self::$document->fgets(); + do { + $method = 'extractFieldContent'; + $buffer = ltrim(self::$line, self::$trim_mask); + if (($buffer[0] ?? '') === self::$enclosure) { + $method = 'extractEnclosedFieldContent'; + self::$line = $buffer; + } + + $record[] = self::$method(); + } while (false !== self::$line); + + return $record; + } + + /** + * Extracts the content from a field without enclosure. * * - Leading and trailing whitespaces must be removed. * - trailing line-breaks must be removed. @@ -157,13 +180,13 @@ private static function extractFieldContent() } /** - * Extract field with enclosure as per RFC4180. + * Extracts the content from a field with enclosure. * * - Field content can spread on multiple document lines. * - Content inside enclosure must be preserved. * - Double enclosure sequence must be replaced by single enclosure character. * - Trailing line break must be removed if they are not part of the field content. - * - Invalid fields content are treated as per fgetcsv behavior. + * - Invalid field content are treated as per fgetcsv behavior. */ private static function extractEnclosedFieldContent(): string { From fefb8d7eba70a245a7eaeae8ce05c8210f5c643b Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Thu, 27 Sep 2018 09:24:45 +0200 Subject: [PATCH 087/858] Improve error message --- src/Polyfill/EmptyEscapeParser.php | 3 ++- tests/Polyfill/EmptyEscapeParserTest.php | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Polyfill/EmptyEscapeParser.php b/src/Polyfill/EmptyEscapeParser.php index 5ba2354d..bd079016 100644 --- a/src/Polyfill/EmptyEscapeParser.php +++ b/src/Polyfill/EmptyEscapeParser.php @@ -128,7 +128,8 @@ private static function filterDocument($document) } throw new TypeError(sprintf( - 'Expected a %s or an SplFileObject object, %s given', + '%s::parse expects parameter 1 to be a %s or a SplFileObject object, %s given', + static::class, Stream::class, is_object($document) ? get_class($document) : gettype($document) )); diff --git a/tests/Polyfill/EmptyEscapeParserTest.php b/tests/Polyfill/EmptyEscapeParserTest.php index 1e5d2cc4..f623200c 100644 --- a/tests/Polyfill/EmptyEscapeParserTest.php +++ b/tests/Polyfill/EmptyEscapeParserTest.php @@ -35,8 +35,8 @@ class EmptyEscapeParserTest extends TestCase public function testConstructorThrowsTypeErrorWithUnknownDocument() { self::expectException(TypeError::class); - foreach (EmptyEscapeParser::parse([]) as $record) { - } + $records = EmptyEscapeParser::parse([]); + $records->rewind(); } /** From f8d64782bac71918f29c987b51c4575f5c114b5f Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Fri, 28 Sep 2018 14:28:03 +0200 Subject: [PATCH 088/858] Improve polyfill matching SplFileObject CSV behavior --- src/Polyfill/EmptyEscapeParser.php | 70 +++++++++++++------- tests/Polyfill/EmptyEscapeParserTest.php | 84 ++++++++++++++---------- 2 files changed, 95 insertions(+), 59 deletions(-) diff --git a/src/Polyfill/EmptyEscapeParser.php b/src/Polyfill/EmptyEscapeParser.php index bd079016..45f563a3 100644 --- a/src/Polyfill/EmptyEscapeParser.php +++ b/src/Polyfill/EmptyEscapeParser.php @@ -32,24 +32,8 @@ use function substr; /** - * A Polyfill to PHP's SplFileObject behavior when reading a CSV document - * with the SplFileObject::READ_CSV and SplFileObject::SKIP_EMPTY flags on - * and the empty string as the escape parameter. - * - * - * $file = new SplFileObject('/path/to/file.csv', 'r'); - * $file->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY); - * $file->setCsvControl($delimiter, $enclosure, ''); //this does not currently in any PHP stable release - * - * - * instead you can do this - * - * - * $file = new SplFileObject('/path/to/file.csv', 'r'); - * $file->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY); - * $file->setCsvControl($delimiter, $enclosure, $escape); - * EmptyEscapeParser::parse($file); //parsing will be done while ignoring the escape character value. - * + * A Polyfill to PHP's SplFileObject to enable parsing the CSV document + * without taking into account the escape character. * * @see https://php.net/manual/en/function.fgetcsv.php * @see https://php.net/manual/en/function.fgets.php @@ -93,6 +77,27 @@ final class EmptyEscapeParser /** * Converts the document into a CSV record iterator. * + * In PH7.4+ you'll be able to do + * + * + * $file = new SplFileObject('/path/to/file.csv', 'r'); + * $file->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY); + * $file->setCsvControl($delimiter, $enclosure, ''); + * foreach ($file as $record) { + * //$record escape mechanism is blocked by the empty string + * } + * + * + * In PHP7.3- you can do + * + * + * $file = new SplFileObject('/path/to/file.csv', 'r'); + * $it = EmptyEscapeParser::parse($file); //parsing will be done while ignoring the escape character value. + * foreach ($it as $record) { + * //fgetcsv is not directly use hence the escape char is not taken into account + * } + * + * * Each record array contains strings elements. * * @param SplFileObject|Stream $document @@ -108,7 +113,7 @@ public static function parse($document): Generator self::$document->rewind(); while (self::$document->valid()) { $record = self::extractRecord(); - if ([null] !== $record) { + if (!in_array(null, $record, true)) { yield $record; } } @@ -159,10 +164,11 @@ private static function extractRecord(): array /** * Extracts the content from a field without enclosure. * - * - Leading and trailing whitespaces must be removed. - * - trailing line-breaks must be removed. + * - Field content can not spread on multiple document lines. + * - Content must be preserved. + * - Trailing line-breaks must be removed. * - * @return null|string + * @return string|null */ private static function extractFieldContent() { @@ -184,12 +190,14 @@ private static function extractFieldContent() * Extracts the content from a field with enclosure. * * - Field content can spread on multiple document lines. - * - Content inside enclosure must be preserved. + * - Content between consecutive enclosure characters must be preserved. * - Double enclosure sequence must be replaced by single enclosure character. * - Trailing line break must be removed if they are not part of the field content. - * - Invalid field content are treated as per fgetcsv behavior. + * - Invalid field content is treated as per fgetcsv behavior. + * + * @return string|null */ - private static function extractEnclosedFieldContent(): string + private static function extractEnclosedFieldContent() { if ((self::$line[0] ?? '') === self::$enclosure) { self::$line = substr(self::$line, 1); @@ -199,10 +207,22 @@ private static function extractEnclosedFieldContent(): string while (false !== self::$line) { list($buffer, $remainder) = explode(self::$enclosure, self::$line, 2) + [1 => false]; $content .= $buffer; + if (false !== $remainder) { self::$line = $remainder; break; } + + if (!self::$document->valid() && $content === $buffer) { + if ($content !== rtrim($content, "\r\n")) { + self::$line = false; + + return $content; + } + + return null; + } + self::$line = self::$document->fgets(); } diff --git a/tests/Polyfill/EmptyEscapeParserTest.php b/tests/Polyfill/EmptyEscapeParserTest.php index f623200c..58776540 100644 --- a/tests/Polyfill/EmptyEscapeParserTest.php +++ b/tests/Polyfill/EmptyEscapeParserTest.php @@ -146,6 +146,39 @@ public function testNoTrimmedSpaceWithNotEncloseField() } } + /** + * @covers ::parse + * @covers ::extractFieldContent + * @covers ::extractEnclosedFieldContent + */ + public function testDoubleEnclosure() + { + $str = <<setCsvControl(';', '"'); + $records = EmptyEscapeParser::parse($stream); + foreach ($records as $offset => $record) { + self::assertSame($expected[$offset], $record); + } + } + + /*********************************************************** + * TEST FOR MATCHING FGETCSV BEHAVIOR WITH INVALID CSV LINES + ************************************************************/ + /** * @covers ::parse * @covers ::extractFieldContent @@ -153,12 +186,10 @@ public function testNoTrimmedSpaceWithNotEncloseField() * * @dataProvider invalidCsvRecordProvider */ - public function testHandlingInvalidCSVwithEnclosure(string $string, array $expected) + public function testHandlingInvalidCSVLikeFgetcsv(string $string, array $expected) { - $stream = Stream::createFromString($string); - foreach (EmptyEscapeParser::parse($stream) as $record) { - self::assertSame($expected, $record); - } + $records = EmptyEscapeParser::parse(Stream::createFromString($string)); + self::assertSame($expected, iterator_to_array($records)[0]); } public function invalidCsvRecordProvider() @@ -172,44 +203,29 @@ public function invalidCsvRecordProvider() 'string' => 'Year,Make,Model,Description,Price"', 'record' => ['Year', 'Make', 'Model', 'Description', 'Price"'], ], - 'enclosure at the end of a record field' => [ - 'string' => 'Year,Make,Model,Description,"Price', - 'record' => ['Year', 'Make', 'Model', 'Description', 'Price'], - ], 'enclosure started but not ended' => [ 'string' => 'Year,Make,Model,Description,"Pri"ce', 'record' => ['Year', 'Make', 'Model', 'Description', 'Price'], ], + 'enclosure ended with a non close enclosure field but with a end line' => [ + 'string' => 'Year,Make,Model,Description,"Price'."\r\n", + 'record' => ['Year', 'Make', 'Model', 'Description', 'Price'."\r\n"], + ], ]; } - /** - * @covers ::parse - * @covers ::extractFieldContent - * @covers ::extractEnclosedFieldContent - */ - public function testDoubleEnclosure() + public function testHandlingInvalidCSVLikeFgetcsvWithMissingEndEnclosureAndEOLAtTheEndOfTheDocument() { - $str = <<setCsvControl(';', '"'); - $records = EmptyEscapeParser::parse($stream); - foreach ($records as $offset => $record) { - self::assertSame($expected[$offset], $record); - } + public function testHandlingInvalidCSVLikeFgetcsvWithMissingEndEnclosureAtTheEndOfTheDocumentMultipleLine() + { + $it = EmptyEscapeParser::parse(Stream::createFromString('Year,Make,Model,Description,"Price'."\r\nfoo,bar")); + $records = iterator_to_array($it, false); + self::assertSame("Price\r\nfoo,bar", $records[0][4]); } /** From daa71dcf2a4b9f09c570de87a701cf0e259dfa56 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Sun, 30 Sep 2018 20:18:15 +0200 Subject: [PATCH 089/858] Improve EmptyEscapeParser test suite --- src/Polyfill/EmptyEscapeParser.php | 18 +++-- src/Writer.php | 2 +- tests/Polyfill/EmptyEscapeParserTest.php | 92 +++++++++--------------- 3 files changed, 47 insertions(+), 65 deletions(-) diff --git a/src/Polyfill/EmptyEscapeParser.php b/src/Polyfill/EmptyEscapeParser.php index 45f563a3..aab5c00d 100644 --- a/src/Polyfill/EmptyEscapeParser.php +++ b/src/Polyfill/EmptyEscapeParser.php @@ -213,17 +213,21 @@ private static function extractEnclosedFieldContent() break; } - if (!self::$document->valid() && $content === $buffer) { - if ($content !== rtrim($content, "\r\n")) { - self::$line = false; + if (self::$document->valid()) { + self::$line = self::$document->fgets(); + continue; + } - return $content; - } + self::$line = false; + if ($content !== $buffer) { + break; + } - return null; + if ($content !== rtrim($content, "\r\n")) { + return $content; } - self::$line = self::$document->fgets(); + return null; } if (in_array(self::$line, self::FIELD_BREAKS, true)) { diff --git a/src/Writer.php b/src/Writer.php index 614107d9..2da7e4c5 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -153,7 +153,7 @@ public function insertAll($records): int public function insertOne(array $record): int { $method = 'addRecord'; - if ('' === $this->escape && PHP_VERSION_ID < 70400) { + if ('' === $this->escape) { $method = 'addRFC4180CompliantRecord'; } diff --git a/tests/Polyfill/EmptyEscapeParserTest.php b/tests/Polyfill/EmptyEscapeParserTest.php index 58776540..adde12eb 100644 --- a/tests/Polyfill/EmptyEscapeParserTest.php +++ b/tests/Polyfill/EmptyEscapeParserTest.php @@ -43,7 +43,7 @@ public function testConstructorThrowsTypeErrorWithUnknownDocument() * @covers \League\Csv\Stream::fgets * @covers ::parse * @covers ::filterDocument - * @covers ::parse + * @covers ::extractRecord * @covers ::extractFieldContent * @covers ::extractEnclosedFieldContent */ @@ -71,6 +71,7 @@ public function testWorksWithMultiLines() /** * @covers \League\Csv\Stream::fgets * @covers ::parse + * @covers ::extractRecord * @covers ::extractFieldContent * @covers ::extractEnclosedFieldContent */ @@ -98,6 +99,7 @@ public function testWorksWithMultiLinesWithDifferentDelimiter() /** * @covers ::parse + * @covers ::extractRecord * @covers ::extractFieldContent * @covers ::extractEnclosedFieldContent */ @@ -125,6 +127,7 @@ public function testKeepEmptyLines() /** * @covers ::parse + * @covers ::extractRecord * @covers ::extractFieldContent * @covers ::extractEnclosedFieldContent */ @@ -148,6 +151,7 @@ public function testNoTrimmedSpaceWithNotEncloseField() /** * @covers ::parse + * @covers ::extractRecord * @covers ::extractFieldContent * @covers ::extractEnclosedFieldContent */ @@ -180,82 +184,56 @@ public function testDoubleEnclosure() ************************************************************/ /** + * @test + * * @covers ::parse + * @covers ::extractRecord * @covers ::extractFieldContent * @covers ::extractEnclosedFieldContent * * @dataProvider invalidCsvRecordProvider */ - public function testHandlingInvalidCSVLikeFgetcsv(string $string, array $expected) + public function it_comparse_polyfill_result_against_php_result($string) { - $records = EmptyEscapeParser::parse(Stream::createFromString($string)); - self::assertSame($expected, iterator_to_array($records)[0]); + $csv = Reader::createFromString($string); + $php_parser_result = iterator_to_array($csv); + $csv->setEscape(''); + $polyfill_result = iterator_to_array($csv); + self::assertEquals($php_parser_result, $polyfill_result); } public function invalidCsvRecordProvider() { + $str = << [ - 'string' => 'Ye"ar,Make",Model,Description,Price', - 'record' => ['Ye"ar', 'Make"', 'Model', 'Description', 'Price'], + 'Ye"ar,Make",Model,Description,Price', ], 'enclosure at the end of a non-unclosed field' => [ - 'string' => 'Year,Make,Model,Description,Price"', - 'record' => ['Year', 'Make', 'Model', 'Description', 'Price"'], + 'Year,Make,Model,Description,Price"', ], 'enclosure started but not ended' => [ - 'string' => 'Year,Make,Model,Description,"Pri"ce', - 'record' => ['Year', 'Make', 'Model', 'Description', 'Price'], + 'Year,Make,Model,Description,"Pri"ce', + ], + 'missing end enclosure at the end of the document' => [ + 'Year,Make,Model,Description,"Price', ], 'enclosure ended with a non close enclosure field but with a end line' => [ - 'string' => 'Year,Make,Model,Description,"Price'."\r\n", - 'record' => ['Year', 'Make', 'Model', 'Description', 'Price'."\r\n"], + 'Year,Make,Model,Description,"Price'."\r\n", + ], + 'missing end enclosure at the en of document with multiline field' => [ + 'Year,Make,Model,Description,"Price'."\r\nfoo,bar", + ], + 'test invalid csv parsing' => [ + '"foo"bar",foo"bar'."\r\n".'"foo"'."\r\n".'baz,bar"', + ], + 'empty string between fields' => [ + $str, ], ]; } - - public function testHandlingInvalidCSVLikeFgetcsvWithMissingEndEnclosureAndEOLAtTheEndOfTheDocument() - { - $it = EmptyEscapeParser::parse(Stream::createFromString('Year,Make,Model,Description,"Price')); - $records = iterator_to_array($it, false); - self::assertSame([], $records); - } - - public function testHandlingInvalidCSVLikeFgetcsvWithMissingEndEnclosureAtTheEndOfTheDocumentMultipleLine() - { - $it = EmptyEscapeParser::parse(Stream::createFromString('Year,Make,Model,Description,"Price'."\r\nfoo,bar")); - $records = iterator_to_array($it, false); - self::assertSame("Price\r\nfoo,bar", $records[0][4]); - } - - /** - * @covers ::parse - * @covers ::extractFieldContent - * @covers ::extractEnclosedFieldContent - */ - public function testInvalidCsvParseAsFgetcsv() - { - $str = '"foo"bar",foo"bar'."\r\n".'"foo"'."\r\n".'baz,bar"'; - $csv = Reader::createFromString($str); - $fgetcsv_records = iterator_to_array($csv); - $csv->setEscape(''); - self::assertEquals($fgetcsv_records, iterator_to_array($csv)); - } - - public function testCsvParsedAsFgetcsv($value='') - { - $str = << $record) { - self::assertEquals($expected[$offset], $record); - } - } } From 737d912a9b92e5d6e50b7603a350c5d536938e71 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Mon, 1 Oct 2018 08:38:32 +0200 Subject: [PATCH 090/858] improve test suite --- tests/Polyfill/EmptyEscapeParserTest.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/Polyfill/EmptyEscapeParserTest.php b/tests/Polyfill/EmptyEscapeParserTest.php index adde12eb..32cef0bd 100644 --- a/tests/Polyfill/EmptyEscapeParserTest.php +++ b/tests/Polyfill/EmptyEscapeParserTest.php @@ -193,13 +193,15 @@ public function testDoubleEnclosure() * * @dataProvider invalidCsvRecordProvider */ - public function it_comparse_polyfill_result_against_php_result($string) + public function it_parses_like_splfileobject_with_invalid_csv($string) { - $csv = Reader::createFromString($string); - $php_parser_result = iterator_to_array($csv); + $spl = new SplTempFileObject(); + $spl->fwrite($string); + $csv = Reader::createFromFileObject($spl); + $spl_result = iterator_to_array($csv); $csv->setEscape(''); $polyfill_result = iterator_to_array($csv); - self::assertEquals($php_parser_result, $polyfill_result); + self::assertEquals($spl_result, $polyfill_result); } public function invalidCsvRecordProvider() From 23e092a45d1398909a85e844f60f08a0659eb07c Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Mon, 1 Oct 2018 09:30:46 +0200 Subject: [PATCH 091/858] Improve Polyfill parser --- src/Polyfill/EmptyEscapeParser.php | 3 +++ tests/ByteSequenceTest.php | 2 +- tests/CharsetConverterTest.php | 2 +- tests/Polyfill/EmptyEscapeParserTest.php | 12 +++--------- tests/RFC4180FieldTest.php | 2 +- tests/ResultSetTest.php | 2 +- tests/WriterTest.php | 2 +- 7 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/Polyfill/EmptyEscapeParser.php b/src/Polyfill/EmptyEscapeParser.php index aab5c00d..55a4f28f 100644 --- a/src/Polyfill/EmptyEscapeParser.php +++ b/src/Polyfill/EmptyEscapeParser.php @@ -232,6 +232,9 @@ private static function extractEnclosedFieldContent() if (in_array(self::$line, self::FIELD_BREAKS, true)) { self::$line = false; + if (!self::$document->valid()) { + return $content; + } return rtrim($content, "\r\n"); } diff --git a/tests/ByteSequenceTest.php b/tests/ByteSequenceTest.php index 2f2307e3..ce255432 100644 --- a/tests/ByteSequenceTest.php +++ b/tests/ByteSequenceTest.php @@ -35,7 +35,7 @@ public function testByteSequenceMatch($str, $expected) self::assertSame($expected, bom_match($str)); } - public function ByteSequenceMatchProvider() + public function ByteSequenceMatchProvider(): array { return [ 'empty string' => [ diff --git a/tests/CharsetConverterTest.php b/tests/CharsetConverterTest.php index 2fb7c742..97d50cb8 100644 --- a/tests/CharsetConverterTest.php +++ b/tests/CharsetConverterTest.php @@ -184,7 +184,7 @@ public function testConvertOnlyStringField(array $record, array $expected) self::assertSame($expected, $res[0]); } - public function converterProvider() + public function converterProvider(): array { return [ 'only numeric values' => [ diff --git a/tests/Polyfill/EmptyEscapeParserTest.php b/tests/Polyfill/EmptyEscapeParserTest.php index 32cef0bd..1087a719 100644 --- a/tests/Polyfill/EmptyEscapeParserTest.php +++ b/tests/Polyfill/EmptyEscapeParserTest.php @@ -179,13 +179,7 @@ public function testDoubleEnclosure() } } - /*********************************************************** - * TEST FOR MATCHING FGETCSV BEHAVIOR WITH INVALID CSV LINES - ************************************************************/ - /** - * @test - * * @covers ::parse * @covers ::extractRecord * @covers ::extractFieldContent @@ -193,7 +187,7 @@ public function testDoubleEnclosure() * * @dataProvider invalidCsvRecordProvider */ - public function it_parses_like_splfileobject_with_invalid_csv($string) + public function testParsesLikeSplFileObjectInvalidCsv($string) { $spl = new SplTempFileObject(); $spl->fwrite($string); @@ -204,7 +198,7 @@ public function it_parses_like_splfileobject_with_invalid_csv($string) self::assertEquals($spl_result, $polyfill_result); } - public function invalidCsvRecordProvider() + public function invalidCsvRecordProvider(): array { $str = << [ 'Year,Make,Model,Description,"Price'."\r\n", ], - 'missing end enclosure at the en of document with multiline field' => [ + 'missing end enclosure at the end of document with multiline field' => [ 'Year,Make,Model,Description,"Price'."\r\nfoo,bar", ], 'test invalid csv parsing' => [ diff --git a/tests/RFC4180FieldTest.php b/tests/RFC4180FieldTest.php index 6c14c8cf..e051598b 100644 --- a/tests/RFC4180FieldTest.php +++ b/tests/RFC4180FieldTest.php @@ -122,7 +122,7 @@ public function testOnCreateFailedWithWrongParams(array $params) self::assertFalse($filter->onCreate()); } - public function wrongParamProvider() + public function wrongParamProvider(): array { return [ 'empty array' => [[ diff --git a/tests/ResultSetTest.php b/tests/ResultSetTest.php index 01ef330e..40ce2b0f 100644 --- a/tests/ResultSetTest.php +++ b/tests/ResultSetTest.php @@ -393,7 +393,7 @@ public function testFetchPairsIteratorMode($key, $value, array $expected) } } - public function fetchPairsDataProvider() + public function fetchPairsDataProvider(): array { return [ 'default values' => [ diff --git a/tests/WriterTest.php b/tests/WriterTest.php index 315aa858..3e0416d6 100644 --- a/tests/WriterTest.php +++ b/tests/WriterTest.php @@ -270,7 +270,7 @@ public function testRFC4180WriterMode(string $expected, array $record) } } - public function compliantRFC4180Provider() + public function compliantRFC4180Provider(): array { return [ 'bug #43225' => [ From 08de1a9ed081be14da033ce259bebfda1b5e694d Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Mon, 1 Oct 2018 10:03:46 +0200 Subject: [PATCH 092/858] Improve Polyfill Parser --- src/Polyfill/EmptyEscapeParser.php | 20 ++++++-------------- tests/Polyfill/EmptyEscapeParserTest.php | 2 +- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/Polyfill/EmptyEscapeParser.php b/src/Polyfill/EmptyEscapeParser.php index 55a4f28f..7fec0418 100644 --- a/src/Polyfill/EmptyEscapeParser.php +++ b/src/Polyfill/EmptyEscapeParser.php @@ -207,9 +207,8 @@ private static function extractEnclosedFieldContent() while (false !== self::$line) { list($buffer, $remainder) = explode(self::$enclosure, self::$line, 2) + [1 => false]; $content .= $buffer; - - if (false !== $remainder) { - self::$line = $remainder; + self::$line = $remainder; + if (false !== self::$line) { break; } @@ -218,16 +217,9 @@ private static function extractEnclosedFieldContent() continue; } - self::$line = false; - if ($content !== $buffer) { - break; - } - - if ($content !== rtrim($content, "\r\n")) { - return $content; + if ($buffer === rtrim($content, "\r\n")) { + return null; } - - return null; } if (in_array(self::$line, self::FIELD_BREAKS, true)) { @@ -240,13 +232,13 @@ private static function extractEnclosedFieldContent() } $char = self::$line[0] ?? ''; - if (self::$delimiter === $char) { + if ($char === self::$delimiter) { self::$line = substr(self::$line, 1); return $content; } - if (self::$enclosure === $char) { + if ($char === self::$enclosure) { return $content.self::$enclosure.self::extractEnclosedFieldContent(); } diff --git a/tests/Polyfill/EmptyEscapeParserTest.php b/tests/Polyfill/EmptyEscapeParserTest.php index 1087a719..5665ecbd 100644 --- a/tests/Polyfill/EmptyEscapeParserTest.php +++ b/tests/Polyfill/EmptyEscapeParserTest.php @@ -103,7 +103,7 @@ public function testWorksWithMultiLinesWithDifferentDelimiter() * @covers ::extractFieldContent * @covers ::extractEnclosedFieldContent */ - public function testKeepEmptyLines() + public function testRemoveEmptyLines() { $source = << Date: Tue, 2 Oct 2018 15:59:17 +0200 Subject: [PATCH 093/858] better formatting static closures --- src/EncloseField.php | 2 +- src/RFC4180Field.php | 4 ++-- src/Reader.php | 4 ++-- src/ResultSet.php | 8 ++++---- src/Stream.php | 2 +- src/functions.php | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/EncloseField.php b/src/EncloseField.php index 80bebae0..d94b4f50 100644 --- a/src/EncloseField.php +++ b/src/EncloseField.php @@ -97,7 +97,7 @@ public static function addTo(Writer $csv, string $sequence): Writer throw new InvalidArgumentException('The sequence must contain at least one character to force enclosure'); } - $formatter = function (array $record) use ($sequence) { + $formatter = static function (array $record) use ($sequence) { foreach ($record as &$value) { $value = $sequence.$value; } diff --git a/src/RFC4180Field.php b/src/RFC4180Field.php index 254fc6db..1a5e60c2 100644 --- a/src/RFC4180Field.php +++ b/src/RFC4180Field.php @@ -112,7 +112,7 @@ public static function addFormatterTo(Writer $csv, string $whitespace_replace): throw new InvalidArgumentException('The sequence contains a character that enforces enclosure or is a CSV control character or is the empty string.'); } - $mapper = function ($value) use ($whitespace_replace) { + $mapper = static function ($value) use ($whitespace_replace) { if (is_string($value)) { return str_replace(' ', $whitespace_replace, $value); } @@ -120,7 +120,7 @@ public static function addFormatterTo(Writer $csv, string $whitespace_replace): return $value; }; - $formatter = function (array $record) use ($mapper): array { + $formatter = static function (array $record) use ($mapper): array { return array_map($mapper, $record); }; diff --git a/src/Reader.php b/src/Reader.php index 154deb33..5d6a3a8f 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -264,7 +264,7 @@ public function jsonSerialize(): array public function getRecords(array $header = []): Iterator { $header = $this->computeHeader($header); - $normalized = function ($record): bool { + $normalized = static function ($record): bool { return is_array($record) && $record != [null]; }; $bom = $this->getInputBOM(); @@ -314,7 +314,7 @@ protected function combineHeader(Iterator $iterator, array $header): Iterator } $field_count = count($header); - $mapper = function (array $record) use ($header, $field_count): array { + $mapper = static function (array $record) use ($header, $field_count): array { if (count($record) != $field_count) { $record = array_slice(array_pad($record, $field_count, null), 0, $field_count); } diff --git a/src/ResultSet.php b/src/ResultSet.php index 96ea9d24..cdb507f9 100644 --- a/src/ResultSet.php +++ b/src/ResultSet.php @@ -144,11 +144,11 @@ public function fetchOne(int $nth_record = 0): array public function fetchColumn($index = 0): Generator { $offset = $this->getColumnIndex($index, __METHOD__.'() expects the column index to be a valid string or integer, `%s` given'); - $filter = function (array $record) use ($offset): bool { + $filter = static function (array $record) use ($offset): bool { return isset($record[$offset]); }; - $select = function (array $record) use ($offset): string { + $select = static function (array $record) use ($offset): string { return $record[$offset]; }; @@ -228,11 +228,11 @@ public function fetchPairs($offset_index = 0, $value_index = 1): Generator $offset = $this->getColumnIndex($offset_index, __METHOD__.'() expects the offset index value to be a valid string or integer, `%s` given'); $value = $this->getColumnIndex($value_index, __METHOD__.'() expects the value index value to be a valid string or integer, `%s` given'); - $filter = function (array $record) use ($offset): bool { + $filter = static function (array $record) use ($offset): bool { return isset($record[$offset]); }; - $select = function (array $record) use ($offset, $value): array { + $select = static function (array $record) use ($offset, $value): array { return [$record[$offset], $record[$value] ?? null]; }; diff --git a/src/Stream.php b/src/Stream.php index 28adeede..55385b60 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -146,7 +146,7 @@ public function __construct($resource) */ public function __destruct() { - $walker = function ($filter): bool { + $walker = static function ($filter): bool { return @stream_filter_remove($filter); }; diff --git a/src/functions.php b/src/functions.php index 85dad6b8..e7d472dd 100644 --- a/src/functions.php +++ b/src/functions.php @@ -60,13 +60,13 @@ function bom_match(string $str): string */ function delimiter_detect(Reader $csv, array $delimiters, int $limit = 1): array { - $found = array_unique(array_filter($delimiters, function (string $value): bool { + $found = array_unique(array_filter($delimiters, static function (string $value): bool { return 1 == strlen($value); })); $stmt = (new Statement())->limit($limit)->where(function (array $record): bool { return count($record) > 1; }); - $reducer = function (array $result, string $delimiter) use ($csv, $stmt): array { + $reducer = static function (array $result, string $delimiter) use ($csv, $stmt): array { $result[$delimiter] = count(iterator_to_array($stmt->process($csv->setDelimiter($delimiter)), false), COUNT_RECURSIVE); return $result; From 9c7e8f523d9383ecd242f18156d20caf35ba981f Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Tue, 2 Oct 2018 21:36:01 +0200 Subject: [PATCH 094/858] improve test suite for is_iterable polyfill --- src/functions.php | 3 +++ tests/CsvTest.php | 21 --------------------- 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/src/functions.php b/src/functions.php index e7d472dd..9f67f8d9 100644 --- a/src/functions.php +++ b/src/functions.php @@ -106,6 +106,9 @@ function is_nullable_int($value): bool use League\Csv; if (PHP_VERSION_ID < 70100 && !function_exists('\is_iterable')) { + /** + * @codeCoverageIgnore + */ function is_iterable($iterable) { return Csv\is_iterable($iterable); diff --git a/tests/CsvTest.php b/tests/CsvTest.php index bb83d648..3bd7b492 100644 --- a/tests/CsvTest.php +++ b/tests/CsvTest.php @@ -20,7 +20,6 @@ use PHPUnit\Framework\TestCase; use SplTempFileObject; use const PHP_EOL; -use const PHP_VERSION; use const STREAM_FILTER_READ; use const STREAM_FILTER_WRITE; use function chr; @@ -30,7 +29,6 @@ use function strtolower; use function tmpfile; use function unlink; -use function version_compare; /** * @group csv @@ -403,23 +401,4 @@ public function testLeagueCsvIsIterable() self::assertFalse(CSVIsiterable((object) ['foo'])); self::assertFalse(CSVIsiterable(Writer::createFromString(''))); } - - /** - * @covers \League\Csv\is_iterable - */ - public function testIsIterablePolyFill() - { - if (!version_compare(PHP_VERSION, '7.1.0', '<')) { - self::markTestSkipped(sprintf('%s test PHP7.0 Polyfill for is_iterable', __METHOD__)); - } - - self::assertTrue(is_iterable(['foo'])); - self::assertTrue(is_iterable(Reader::createFromString(''))); - self::assertTrue(is_iterable((function () { - yield 1; - })())); - self::assertFalse(is_iterable(1)); - self::assertFalse(is_iterable((object) ['foo'])); - self::assertFalse(is_iterable(Writer::createFromString(''))); - } } From 5e69545d55d440e2862b90e9bf9ba6c91333d41e Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Wed, 3 Oct 2018 09:21:09 +0200 Subject: [PATCH 095/858] improve codebase --- src/AbstractCsv.php | 3 +-- src/Polyfill/EmptyEscapeParser.php | 2 +- src/Reader.php | 2 +- src/Stream.php | 3 +-- src/functions.php | 2 +- 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/AbstractCsv.php b/src/AbstractCsv.php index 6ca3a6b1..bca407ef 100644 --- a/src/AbstractCsv.php +++ b/src/AbstractCsv.php @@ -22,7 +22,6 @@ use const FILTER_FLAG_STRIP_LOW; use const FILTER_SANITIZE_STRING; use function filter_var; -use function get_class; use function mb_strlen; use function rawurlencode; use function sprintf; @@ -124,7 +123,7 @@ public function __destruct() */ public function __clone() { - throw new Exception(sprintf('An object of class %s cannot be cloned', get_class($this))); + throw new Exception(sprintf('An object of class %s cannot be cloned', static::class)); } /** diff --git a/src/Polyfill/EmptyEscapeParser.php b/src/Polyfill/EmptyEscapeParser.php index 7fec0418..39a0e823 100644 --- a/src/Polyfill/EmptyEscapeParser.php +++ b/src/Polyfill/EmptyEscapeParser.php @@ -134,7 +134,7 @@ private static function filterDocument($document) throw new TypeError(sprintf( '%s::parse expects parameter 1 to be a %s or a SplFileObject object, %s given', - static::class, + self::class, Stream::class, is_object($document) ? get_class($document) : gettype($document) )); diff --git a/src/Reader.php b/src/Reader.php index 5d6a3a8f..dcacb885 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -336,7 +336,7 @@ protected function stripBOM(Iterator $iterator, string $bom): Iterator $bom_length = mb_strlen($bom); $mapper = function (array $record, int $index) use ($bom_length): array { - if (0 != $index) { + if (0 !== $index) { return $record; } diff --git a/src/Stream.php b/src/Stream.php index 55385b60..f612f878 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -34,7 +34,6 @@ use function fread; use function fseek; use function fwrite; -use function get_class; use function get_resource_type; use function gettype; use function is_resource; @@ -164,7 +163,7 @@ public function __destruct() */ public function __clone() { - throw new Exception(sprintf('An object of class %s cannot be cloned', get_class($this))); + throw new Exception(sprintf('An object of class %s cannot be cloned', static::class)); } /** diff --git a/src/functions.php b/src/functions.php index 9f67f8d9..6638f675 100644 --- a/src/functions.php +++ b/src/functions.php @@ -63,7 +63,7 @@ function delimiter_detect(Reader $csv, array $delimiters, int $limit = 1): array $found = array_unique(array_filter($delimiters, static function (string $value): bool { return 1 == strlen($value); })); - $stmt = (new Statement())->limit($limit)->where(function (array $record): bool { + $stmt = (new Statement())->limit($limit)->where(static function (array $record): bool { return count($record) > 1; }); $reducer = static function (array $result, string $delimiter) use ($csv, $stmt): array { From fcbd55614375b5da6c307b9609a9850803bbbf91 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Wed, 3 Oct 2018 21:46:13 +0200 Subject: [PATCH 096/858] bye bye jQuery --- docs/_layouts/default.html | 13 +++++-------- docs/_layouts/homepage.html | 5 +---- docs/_layouts/redirect.html | 5 +---- docs/custom.js | 27 +++++++++++++-------------- 4 files changed, 20 insertions(+), 30 deletions(-) diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index 87a0a0fe..de70de11 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -4,6 +4,10 @@ + {% if site.data.project.description %} + + {% endif %} + {% assign version = page.url | remove_first: "/" | split: "/" | first %} {% if version == '' or version == 'upgrading' %} @@ -18,10 +22,6 @@ {{ page.title }} - {{ site.data.project.title }} {% endif %} - {% if site.data.project.description %} - - {% endif %} - {% if site.data.images.favicon %} {% else %} @@ -117,9 +117,6 @@

{{ section[0] }}

© Copyright The League of Extraordinary Packages. Site design by Jonathan Reinink. - - - - + \ No newline at end of file diff --git a/docs/_layouts/homepage.html b/docs/_layouts/homepage.html index 4e21f91d..111a692c 100644 --- a/docs/_layouts/homepage.html +++ b/docs/_layouts/homepage.html @@ -4,8 +4,8 @@ - {{ site.data.project.tagline }} - {{ site.data.project.title }} + {{ site.data.project.tagline }} - {{ site.data.project.title }} @@ -131,8 +131,5 @@

Questions?

© Copyright The League of Extraordinary Packages. Site design by Jonathan Reinink. - - - \ No newline at end of file diff --git a/docs/_layouts/redirect.html b/docs/_layouts/redirect.html index 46769946..bb1fbfdf 100644 --- a/docs/_layouts/redirect.html +++ b/docs/_layouts/redirect.html @@ -4,9 +4,9 @@ - {{ site.data.project.tagline }} - {{ site.data.project.title }} + {{ site.data.project.tagline }} - {{ site.data.project.title }} @@ -56,8 +56,5 @@

Questions?

© Copyright The League of Extraordinary Packages. Site design by Jonathan Reinink. - - - \ No newline at end of file diff --git a/docs/custom.js b/docs/custom.js index 0de8b316..f02a0b69 100644 --- a/docs/custom.js +++ b/docs/custom.js @@ -1,20 +1,19 @@ -$(function() { - $('.versions').change(function (e) { - location.href = $(this).find('option:selected').data('url'); - }); -}); +(() => { + const uri = new URL(location.href); -(function (w) { - var d = w.document, - headerList = d.querySelector('main').querySelectorAll("h2[id]"), - uri = location.href.split('#', 2).pop(); + document.querySelector('.versions').addEventListener('change', function() { + uri.hash = ''; + uri.pathname = this.options[this.options.selectedIndex].dataset.url; + location.href = uri.toString(); + }, false); - for (var i = 0, header, link; header = headerList[i]; ++i) { - link = d.createElement("a"); + document.querySelectorAll("main h2[id]").forEach((header) => { + uri.hash = header.id; + let link = document.createElement("a"); link.className = "header-permalink"; link.title = "Permalink"; - link.href = uri + "#" + header.id; + link.href = uri.toString(); link.innerHTML = "¶"; header.appendChild(link); - } -})(window); \ No newline at end of file + }); +})(); \ No newline at end of file From 4e2c3b1c80906c31ac0593876dc180b3565ac466 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Fri, 5 Oct 2018 08:56:32 +0200 Subject: [PATCH 097/858] update CSV documentation layout --- docs/_layouts/default.html | 21 ++++++++++++--------- docs/custom.css | 27 ++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index de70de11..9c538ff2 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -34,7 +34,7 @@ {% endif %} - + {% if site.data.project.google_analytics_tracking_id %} + \ No newline at end of file diff --git a/docs/custom.css b/docs/custom.css index a474c954..6213abdd 100644 --- a/docs/custom.css +++ b/docs/custom.css @@ -3,6 +3,9 @@ menu .documentation { } @media screen and (max-width: 549px) { + header { + padding: 20px 10px 25px 10px; + } menu { text-align: center; } @@ -31,7 +34,8 @@ blockquote { /** --------------- select menu --------- */ .tool { - width:225px; + display:none; + width:300px; margin:15px auto; padding:0 45px; } @@ -44,19 +48,22 @@ blockquote { text-transform: uppercase; } - @media screen and (min-width: 550px) { .tool { + display:block; margin: 15px 0 0 30px; position:absolute; top:0; right: 5px; } + .versions-small { + display:none; + } } @media screen and (min-width: 1200px) { .tool { - left: 985px; + left: 980px; } } diff --git a/docs/custom.js b/docs/custom.js index f02a0b69..662f3452 100644 --- a/docs/custom.js +++ b/docs/custom.js @@ -3,17 +3,21 @@ document.querySelector('.versions').addEventListener('change', function() { uri.hash = ''; - uri.pathname = this.options[this.options.selectedIndex].dataset.url; + let path = this.options[this.options.selectedIndex].dataset.url; + if (undefined === path) { + return; + } + uri.pathname = path; location.href = uri.toString(); }, false); - document.querySelectorAll("main h2[id]").forEach((header) => { - uri.hash = header.id; + document.querySelectorAll("main h2[id]").forEach((heading) => { + uri.hash = heading.id; let link = document.createElement("a"); link.className = "header-permalink"; link.title = "Permalink"; link.href = uri.toString(); link.innerHTML = "¶"; - header.appendChild(link); + heading.appendChild(link); }); })(); \ No newline at end of file diff --git a/docs/upgrading/changelog.md b/docs/upgrading/changelog.md deleted file mode 100644 index a2f38d11..00000000 --- a/docs/upgrading/changelog.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -layout: default -title: Changelog -redirect_from: /changelog/ ---- - -# Changelog - -All Notable changes to `Csv` will be documented in this file - -{% for release in site.github.releases %} -## {{ release.name }} - {{ release.published_at | date: "%Y-%m-%d" }} -{{ release.body | replace:'```':'~~~' | markdownify }} -{% endfor %} \ No newline at end of file diff --git a/docs/upgrading/index.md b/docs/upgrading/index.md index 9ec45817..98c36cec 100644 --- a/docs/upgrading/index.md +++ b/docs/upgrading/index.md @@ -1,9 +1,23 @@ --- layout: default -title: Upgrading +title: Release Notes +redirect_from: + - /changelog/ + - /upgrading/changelog/ --- # Upgrading -Welcome to the upgrade guide for `Csv`. We've tried to cover all backward compatible breaks from 5.0 through to the current MAJOR stable release. If we've missed anything, feel free to create an issue, or send a pull request. You can also refer to the information found in the [CHANGELOG.md](https://github.com/thephpleague/csv/blob/master/CHANGELOG.md) file attached to the library. +We've tried to cover all backward compatible breaks from 5.0 through to the current MAJOR stable release. If we've missed anything, feel free to create an issue, or send a pull request. You can also refer to the information found in the [CHANGELOG.md](https://github.com/thephpleague/csv/blob/master/CHANGELOG.md) file attached to the library. +- [Upgrading guide from 8.x to 9.x](/9.0/upgrading/) +- [Upgrading guide from 7.x to 8.x](/8.0/upgrading/) +- [Upgrading guide from 6.x to 7.x](/7.0/upgrading/) +- [Upgrading guide from 5.x to 6.x](/upgrading/6.0/) + +# Release Notes + +{% for release in site.github.releases %} +## {{ release.name }} - {{ release.published_at | date: "%Y-%m-%d" }} +{{ release.body | replace:'```':'~~~' | markdownify }} +{% endfor %} \ No newline at end of file From 6a13b9a26ddf07250ef51f4620f1164543692279 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Fri, 5 Oct 2018 10:45:27 +0200 Subject: [PATCH 099/858] header fixed --- docs/_data/menu.yml | 6 +++--- docs/_layouts/default.html | 21 ++++++++++----------- docs/custom.css | 34 ++++++++++++++++++++++++++-------- 3 files changed, 39 insertions(+), 22 deletions(-) diff --git a/docs/_data/menu.yml b/docs/_data/menu.yml index 0980e2c0..4d2a1c17 100644 --- a/docs/_data/menu.yml +++ b/docs/_data/menu.yml @@ -4,7 +4,7 @@ version: Overview: '/9.0/' Installation: '/9.0/installation/' Upgrading from 8.x: '/9.0/upgrading/' - Changelog: '/upgrading/changelog/' + Changelog: '/upgrading/' Connections Settings: Overview: '/9.0/connections/' Document Loading: '/9.0/connections/instantiation/' @@ -35,7 +35,7 @@ version: Overview: '/8.0/' Installation: '/8.0/installation/' Upgrading from 7.x: '/8.0/upgrading/' - Changelog: '/upgrading/changelog/' + Changelog: '/upgrading/' The API: Instantiation: '/8.0/instantiation/' CSV Properties: '/8.0/properties/' @@ -51,7 +51,7 @@ version: Overview: '/7.0/' Installation: '/7.0/installation/' Upgrading from 6.x: '/7.0/upgrading/' - Changelog: '/upgrading/changelog/' + Changelog: '/upgrading/' The API: Instantiation: '/7.0/instantiation/' CSV Properties: '/7.0/properties/' diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index 1583d637..69526202 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -58,19 +58,18 @@ {{ site.data.project.title }} {{ site.data.project.tagline }} - +
+ +
-
- -
- -
-
-

Questions?

-

{{ site.data.project.title }} was created by {{ site.data.project.author.name }}. Find him on Twitter at @{{ site.data.project.author.twitter_account }}.

-
-
-
diff --git a/docs/homepage.css b/docs/homepage.css index 7ec098fe..865b67c1 100644 --- a/docs/homepage.css +++ b/docs/homepage.css @@ -10,6 +10,7 @@ blockquote { footer { text-align: center; + color:#fff; } /* -------- homepage css -----------------*/ @@ -94,17 +95,18 @@ footer { } .features { - background:#fff; + background: #F2F7FC; + border-bottom:1px solid #CFE4F9; color:#666; font:normal 18px/1.5 "Museo 300"; } .features h1 { - color:#ffb554; + color:#6abcdd; } .features h2 { - color:#ffb554; + color:#6abcdd; font-weight: normal; } @@ -117,7 +119,8 @@ footer { } .highlights { - background:#eff0f1; + background:#fff; + border-bottom:1px solid #CFE4F9; } .highlights .description { @@ -151,12 +154,14 @@ footer { } .documentation { - background: #6abcdd; + background: #F2F7FC; + border-bottom:1px solid #CFE4F9; } .documentation h1 { margin-bottom:.5em; font-size:36px; + color:#6abcdd; } .documentation .footnote { @@ -282,13 +287,14 @@ footer { .questions { padding:0; - background:#283b4f; - color:#fff; + background:#fff; + color:#666; + border-bottom:1px solid #CFE4F9; } .questions h1 { font-size:36px; - color:#6abcdd; + color:#ff4043; } .questions a { @@ -349,7 +355,6 @@ footer { } @media screen and (min-width: 910px) { - .documentation .version { float:left; width:30%; From dbe59391ba92762ea55a46e3e8e99dfcb5ddc4d3 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Sun, 7 Oct 2018 21:42:32 +0200 Subject: [PATCH 110/858] simplify documentation --- docs/9.0/connections/bom.md | 13 --------- docs/9.0/connections/index.md | 29 ------------------- docs/9.0/converter/charset.md | 15 ---------- docs/9.0/converter/html.md | 16 +--------- docs/9.0/converter/xml.md | 14 +-------- docs/9.0/interoperability/enclose-field.md | 12 +------- .../escape-formula-injection.md | 13 --------- docs/9.0/interoperability/rfc4180-field.md | 12 -------- docs/9.0/reader/index.md | 15 ---------- docs/9.0/reader/resultset.md | 13 --------- docs/9.0/reader/statement.md | 13 --------- docs/9.0/writer/helpers.md | 11 ------- docs/9.0/writer/index.md | 16 ---------- 13 files changed, 3 insertions(+), 189 deletions(-) diff --git a/docs/9.0/connections/bom.md b/docs/9.0/connections/bom.md index 4d300a13..e89842e3 100644 --- a/docs/9.0/connections/bom.md +++ b/docs/9.0/connections/bom.md @@ -7,19 +7,6 @@ title: BOM sequence detection and addition ## Detecting the BOM sequence -~~~php -BOM sequence. ### Constants diff --git a/docs/9.0/connections/index.md b/docs/9.0/connections/index.md index 2e7481ac..682bcf16 100644 --- a/docs/9.0/connections/index.md +++ b/docs/9.0/connections/index.md @@ -5,35 +5,6 @@ title: CSV documents configurations # Overview -~~~php -Available since version 9.1.0

-~~~php -Changing the CSV objects control characters after registering the stream filter may result in unexpected returned records.

diff --git a/docs/9.0/interoperability/escape-formula-injection.md b/docs/9.0/interoperability/escape-formula-injection.md index b98ad92b..2e257597 100644 --- a/docs/9.0/interoperability/escape-formula-injection.md +++ b/docs/9.0/interoperability/escape-formula-injection.md @@ -7,19 +7,6 @@ title: CSV Formula Injection

Available since version 9.1.0

-~~~php -This class is deprecated as of version 9.2.0. Please use directly the setEscape method with the empty escape character argument instead with the Reader or the Writer object.

-~~~php -Changing the CSV objects control characters after registering the stream filter may result in unexpected returned records.

- ## Usage with League\CSV objects ~~~php diff --git a/docs/9.0/reader/index.md b/docs/9.0/reader/index.md index bc35e4eb..f350ffd3 100644 --- a/docs/9.0/reader/index.md +++ b/docs/9.0/reader/index.md @@ -5,21 +5,6 @@ title: CSV document Reader connection # Reader Connection -~~~php -Starting with version 9.1.0, createFromPath when used from the Reader object will have its default set to r.

diff --git a/docs/9.0/reader/resultset.md b/docs/9.0/reader/resultset.md index dbf535f2..3bea579c 100644 --- a/docs/9.0/reader/resultset.md +++ b/docs/9.0/reader/resultset.md @@ -5,19 +5,6 @@ title: Accessing Records from a CSV document # Result Set -~~~php -When inserting records into a CSV document using League\Csv\Writer, first insert all the data that need to be inserted before starting manipulating the CSV. If you manipulate your CSV document before insertion, you may change the file cursor position and erase your data.

From ad32b0be18b11c22e30050e692317aad6f42fdc1 Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Mon, 8 Oct 2018 08:30:01 +0200 Subject: [PATCH 111/858] update code snippet css --- docs/_layouts/default.html | 2 +- docs/custom.css | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index 942bdfea..1677927d 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -36,7 +36,7 @@ {% endif %} - + {% if site.data.project.google_analytics_tracking_id %} -{% endif %}
diff --git a/docs/_layouts/homepage.html b/docs/_layouts/homepage.html index b0c223ae..d60d9c15 100644 --- a/docs/_layouts/homepage.html +++ b/docs/_layouts/homepage.html @@ -5,12 +5,12 @@ + {{ site.data.project.tagline }} - {{ site.data.project.title }} -{% if site.data.project.google_analytics_tracking_id %} -{% endif %} diff --git a/docs/_layouts/redirect.html b/docs/_layouts/redirect.html index fd78d721..636710d2 100644 --- a/docs/_layouts/redirect.html +++ b/docs/_layouts/redirect.html @@ -4,15 +4,15 @@ - + + {{ site.data.project.tagline }} - {{ site.data.project.title }} -{% if site.data.project.google_analytics_tracking_id %} -{% endif %} From 1e3ad3e1787e8f8964094fa8eedff76c9886b65b Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Sat, 13 Oct 2018 08:30:18 +0200 Subject: [PATCH 116/858] update doc --- docs/9.0/connections/bom.md | 12 ---- docs/9.0/connections/controls.md | 20 ------- docs/9.0/connections/filters.md | 12 +--- docs/9.0/connections/index.md | 6 -- docs/9.0/connections/instantiation.md | 16 ----- docs/9.0/connections/output.md | 22 ------- docs/9.0/converter/charset.md | 16 ----- docs/9.0/converter/html.md | 2 - docs/9.0/converter/xml.md | 10 ---- docs/9.0/index.md | 10 ---- docs/9.0/installation.md | 2 - docs/9.0/interoperability/enclose-field.md | 8 --- docs/9.0/interoperability/encoding.md | 4 -- .../escape-formula-injection.md | 6 -- docs/9.0/interoperability/rfc4180-field.md | 14 ----- docs/9.0/reader/index.md | 32 ---------- docs/9.0/reader/resultset.md | 32 ---------- docs/9.0/reader/statement.md | 16 ----- docs/9.0/upgrading.md | 60 ------------------- docs/9.0/writer/helpers.md | 6 -- docs/9.0/writer/index.md | 24 -------- docs/_layouts/default.html | 8 +-- docs/_layouts/homepage.html | 2 +- docs/_layouts/redirect.html | 2 +- docs/custom.css | 27 ++++++--- docs/homepage.css | 15 ++++- 26 files changed, 37 insertions(+), 347 deletions(-) diff --git a/docs/9.0/connections/bom.md b/docs/9.0/connections/bom.md index e89842e3..a0032b9c 100644 --- a/docs/9.0/connections/bom.md +++ b/docs/9.0/connections/bom.md @@ -22,16 +22,12 @@ The `ByteSequence` interface provides the following constants : ### bom_match ~~~php -getInputBOM(); ### Setting the outputted BOM sequence ~~~php -All connections classes implement the ByteSequence interface.

~~~php -getDelimiter(); //returns ";" ### Description ~~~php -getEscape(); //returns "\"

setEscape will throw a Exception exception if the submitted string length is not equal to 1 byte or the empty string.

~~~php -getEscape(); //returns "" When using a `SplFileObject`, the returned `AbstractCsv` object will inherit the object underlying CSV controls. ~~~php -setFlags(SplFileObject::READ_CSV); $file->setCsvControl('|', "'", "\\"); @@ -123,8 +107,6 @@ echo $csv->getEscape(); //display '\' ## Detecting the delimiter character ~~~php -getStreamFilterMode(); //return STREAM_FILTER_WRITE Here's a table to quickly determine if PHP stream filters works depending on how the CSV object was instantiated. -| Named constructor | `supportsStreamFilter` | +| Named constructor | supports stream | |------------------------|------------------------| | `createFromString` | true | | `createFromPath ` | true | @@ -56,8 +52,6 @@ Here's a table to quickly determine if PHP stream filters works depending on how ## Adding a stream filter ~~~php -9.1.0, $open_mode default to: ## Loading from a resource stream ~~~php -getContent(); #### Using the `__toString` method ~~~php -if the $length parameter is not a positive integer a OutOfRangeException will be thrown.

~~~php -Be sure to adapt the following code to your own framework/situation. The following code is given as an example without warranty of it working out of the box.

~~~php -insertOne(["foo", "bébé", "jouet"]); ## CharsetConverter as a PHP stream filter ~~~php -output('mycsvfile.csv'); //outputting a CSV Document with all its field ## Usage with PHP stream resources ~~~php -getContent(); On a MacOS system, MS Excel requires a CSV encoded in `UTF-16 LE` using the `tab` character as delimiter. Here's an example on how to meet those requirements using the `League\Csv` package. ~~~php -getContent(); You can use the `EscapeFormula` to format your records before callng `fputcsv` or `SplFileObject::fputcsv`. ~~~php -getContent(); //display 'foo bar,bar' instead of '"foo bar",bar'

The $whitespace_replace sequence should be a sequence not present in the inserted records, otherwise your CSV content will be affected by it.

~~~php -getContent(); //display ' o bar,baz' instead of foo bar,baz Conversely, to read a RFC4180 compliant CSV document, when using the `League\Csv\Reader` object, just need to add the `League\Csv\RFC4180Field` stream filter as shown below: ~~~php -Because the header is lazy loaded, if you provide a positive offset for an invalid record a Exception exception will be triggered when trying to access the invalid record.

~~~php -getHeader(); //triggers a Exception exception ## CSV records ~~~php - $record) { If you specify the CSV header offset using `setHeaderOffset`, the found record will be combined to each CSV record to return an associated array whose keys are composed of the header values. ~~~php - $record) { Conversely, you can submit your own header record using the optional `$header` argument of the `getRecords` method. ~~~php - $record) {

The optional $header argument from the Reader::getRecords takes precedence over the header offset property but its corresponding record will still be removed from the returned Iterator.

~~~php - $record) { You can retrieve the number of records contains in a CSV document using PHP's `count` function because the `Reader` class implements the `Countable` interface. ~~~php -process($reader); The `Reader` class implements the `JsonSerializable` interface. As such you can use the `json_encode` function directly on the instantiated object. The interface is implemented using PHP's `iterator_array` on the `Reader::getRecords` method. As such, the returned `JSON` string data depends on the presence or absence of a header. ~~~php -getHeader(); // is empty because no header information was given #### Example: header information given by the Reader object ~~~php -getHeader(); // returns ['First Name', 'Last Name', 'E-mail']; #### Example: header information given by the Statement object ~~~php -getHeader(); // returns ['Prénom', 'Nom', 'E-mail']; The `ResultSet` class implements implements the `Countable` interface. ~~~php -process($reader)->fetchOne(3); ## Selecting a single column ~~~php -fetchColumn('E-mail') as $value) {

If for a given record the column value is null, the record will be skipped.

~~~php -fetchColumn(2), false)); //returns 5

If the ResultSet contains column names and the $columnIndex is not found an Exception exception is thrown.

~~~php -fetchColumn('foobar') as $record) { `ResultSet::fetchPairs` method returns a `Generator` of key-value pairs. ~~~php -fetchPairs() as $firstname => $lastname) { The `ResultSet` class implements the `JsonSerializable` interface. As such you can use the `json_encode` function directly on the instantiated object. The interface is implemented using PHP's `iterator_array` on the `ResultSet::getRecords` method. As such, the returned `JSON` string data is affected by the presence or absence of column names. ~~~php -process($reader); Just like the `Reader:getRecords`, the `Statement::process` method takes an optional `$header` argument to allow mapping CSV fields name to user defined header record. ~~~php -insertAll([$str]); After: ~~~php -fetchAssoc() as $records) { After: ~~~php -fetchAssoc(['firstname', 'lastname', 'email']); After: ~~~php -fetchAll() as $key => $value) { After: ~~~php - $value) { After: ~~~php -fetchDelimitersOccurrence([',', ';', "\t"], 10); After: ~~~php -fetchColumn(0, function ($value) { After: ~~~php -isActiveStreamFilter(); //true After: ~~~php -prependStreamFilter('string.rot13'); After: ~~~php -clearStreamFilters(); After: ~~~php -toXML(); //$dom is a DOMDocument After: ~~~php -addValidator($validator, 'columns_consistency'); After: ~~~php -insertOne(["foo", "bar"]); //will trigger a CannotInsertRecord exceptio [League\Csv\CharsetConverter](/9.0/converter/charset/) will help you encode your records depending on your settings. ~~~php -insertOne(["foo", "bébé", "jouet"]); If your `Writer` object supports PHP stream filters then it's recommended to use the `CharsetConverter` object via the library [stream filtering mechanism](/9.0/connections/filters/) instead as shown below. ~~~php -Since version 9.2.0 you can provide an empty string for the escape character to enable better RFC4180 compliance.

~~~php -getContent(); A validator is a `callable` which takes single CSV record as an `array` as its sole argument and returns a `boolean` to indicate if it satisfies the validator inner rules. ~~~php - -{% if page.url == '/' or page.url == version_home %} - {{ site.data.project.title }} - {{ site.data.project.tagline }} -{% else %} {{ page.title }} - {{ site.data.project.title }} -{% endif %} - + + + diff --git a/docs/custom.css b/docs/custom.css index d5f29b82..7ab76396 100644 --- a/docs/custom.css +++ b/docs/custom.css @@ -24,13 +24,57 @@ header { background:#fff; } -header nav { +header .header-content:after { + content: ""; + display: table; + clear: both; +} + +header .title { + float: left; + text-align:left; + font-family: "Museo 100"; + font-weight: bold; + margin:0; + padding:0; + width:96px; +} + +header .title a { + color:#ff4143; + text-decoration: none; +} + +header .title a span { + color:#1672ce; +} + +header .search { + float:left; + text-align:right; + width:calc(100% - 175px); + padding-right:.3em; +} + +#doc-search { + font-family: "Museo Sans 500"; + font-weight: normal; + font-size: 16px; + border:1px solid #e8e8e8; + background-color:#f4f4f4; + padding:.5em 1em; + margin-left:.3em; + border-radius: .3em; +} + +header .versions { + float:left; display:none; font-family: "Museo Sans 500"; font-weight: normal; } -header nav h2 { +header .versions h2 { font-family: "Museo Sans 500"; font-weight: normal; font-size: 16px; @@ -45,26 +89,25 @@ header nav h2 { cursor: pointer; } -header nav ul { +header .versions ul { display:none; - z-index: 5; margin:0; padding:0; list-style:none; width: 75px; } -header nav .show { +header .versions .show { display:block; } -header nav li { +header .versions li { margin:0; padding:0; text-align: center; } -header nav a { +header .versions a { display:block; margin:0; padding:.5em .3em; @@ -75,32 +118,19 @@ header nav a { border-width:0 1px 1px; } -header nav a:hover { +header .versions a:hover { background:#f1f1f1; } -header nav li:first-of-type a { +header .versions li:first-of-type a { border-top-width: 1px; border-radius:.3em .3em 0 0; } -header nav li:last-of-type a { +header .versions li:last-of-type a { border-radius:0 0 .3em .3em; } -header .logo { - text-align:left; -} - -header .logo .name { - font-size:38px; -} - -header .logo em { - color:#777; - font-style:normal; -} - label[for=menu] { position:fixed; z-index: 2; @@ -243,17 +273,13 @@ main menu ul li.selected a:hover { } } -@media screen and (min-width: 404px) { - header nav { - display:block; - position:fixed; - z-index: 2; - top:20px; - right:50px; - padding:0; - margin:0; +@media screen and (max-width: 400px) { + header .title { + display: none; } +} +@media screen and (min-width: 404px) { main menu .versions-small { display:none; } @@ -271,22 +297,8 @@ main menu ul li.selected a:hover { padding:1em; } - header .header-content:after { - content: ""; - display: table; - clear: both; - } - - header .logo { - float:left; - text-align:right; - } - - header nav { - position: static; - float:right; - margin:0; - padding:0; + header .versions { + display: block; } label[for=menu] { @@ -301,6 +313,10 @@ main menu ul li.selected a:hover { border-right: 1px dashed #cfe4f9; } + main menu .versions-small { + display:none; + } + main article p, main article li { font-size:.9em; From a3df6b75f5ea321255d63e166ed276428b2a6adc Mon Sep 17 00:00:00 2001 From: Ignace Nyamagana Butera Date: Thu, 20 Feb 2020 23:05:19 +0100 Subject: [PATCH 217/858] Improve documentation layout --- docs/_layouts/default.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index 126fd5a9..79e61b9e 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -30,7 +30,7 @@
-

League\{{ site.data.project.title }}

+

League{{ site.data.project.title }}

- + +