From b5214d08bc7bb1a4d46cb6b597bce300ae235ac3 Mon Sep 17 00:00:00 2001 From: Eliurkis Diaz Date: Tue, 14 May 2019 15:23:39 -0400 Subject: [PATCH 01/10] Add ability to set columns width (#1) --- composer.json | 22 +++++++--- src/Exportable.php | 47 ++++++++++++++++++++-- src/Facades/FastExcel.php | 2 +- src/FastExcel.php | 10 ++++- src/Importable.php | 2 +- src/Providers/FastExcelServiceProvider.php | 2 +- src/SheetCollection.php | 2 +- src/functions/fastexcel.php | 2 +- tests/Dumb.php | 2 +- tests/FastExcelTest.php | 6 +-- tests/IssuesTest.php | 8 ++-- 11 files changed, 80 insertions(+), 25 deletions(-) diff --git a/composer.json b/composer.json index 3cee064..d672978 100644 --- a/composer.json +++ b/composer.json @@ -1,20 +1,26 @@ { - "name": "rap2hpoutre/fast-excel", + "name": "smart145/fast-excel", "type": "library", - "keywords": ["laravel", "excel", "csv", "xls", "xlsx"], + "keywords": [ + "laravel", + "excel", + "csv", + "xls", + "xlsx" + ], "description": "Fast Excel import/export for Laravel", "require": { "php": "^7.0", "illuminate/database": "5.3.* || 5.4.* || 5.5.* || 5.6.* || 5.7.* || 5.8.*", "illuminate/support": "5.3.* || 5.4.* || 5.5.* || 5.6.* || 5.7.* || 5.8.*", - "box/spout": "^2.7" + "smart145/spout": "^2.7.4" }, "require-dev": { "phpunit/phpunit": "^6.4" }, "autoload": { "psr-4": { - "Rap2hpoutre\\FastExcel\\": "src/" + "Smart145\\FastExcel\\": "src/" }, "files": [ "src/functions/fastexcel.php" @@ -22,13 +28,13 @@ }, "autoload-dev": { "psr-4": { - "Rap2hpoutre\\FastExcel\\Tests\\": "tests/" + "Smart145\\FastExcel\\Tests\\": "tests/" } }, "extra": { "laravel": { "providers": [ - "Rap2hpoutre\\FastExcel\\Providers\\FastExcelServiceProvider" + "Smart145\\FastExcel\\Providers\\FastExcelServiceProvider" ] } }, @@ -37,6 +43,10 @@ { "name": "rap2h", "email": "raphaelht@gmail.com" + }, + { + "name": "Dariel Ramos", + "email": "vdariel90@gmail.com" } ], "minimum-stability": "stable" diff --git a/src/Exportable.php b/src/Exportable.php index f325942..adf6c1d 100644 --- a/src/Exportable.php +++ b/src/Exportable.php @@ -1,9 +1,10 @@ columns_width = $widths; + + return $this; + } + /** * @param $path - * @param string $function + * @param string $function * @param callable|null $callback * * @throws \Box\Spout\Common\Exception\IOException @@ -89,8 +102,11 @@ private function exportOrDownload($path, $function, callable $callback = null) { $writer = WriterFactory::create($this->getType($path)); $this->setOptions($writer); + $this->customizeColumnsWidth($writer); /* @var \Box\Spout\Writer\WriterInterface $writer */ $writer->$function($path); + $writer->getCurrentSheet()->setName($this->sheet); + $has_sheets = ($writer instanceof \Box\Spout\Writer\XLSX\Writer || $writer instanceof \Box\Spout\Writer\ODS\Writer); @@ -110,7 +126,7 @@ private function exportOrDownload($path, $function, callable $callback = null) // Add header row. if ($this->with_header) { $first_row = $collection->first(); - $keys = array_keys(is_array($first_row) ? $first_row : $first_row->toArray()); + $keys = $this->hasColumnsHeader() ? array_keys($this->columns_width) : array_keys(is_array($first_row) ? $first_row : $first_row->toArray()); if ($this->header_style) { $writer->addRowWithStyle($keys, $this->header_style); } else { @@ -129,6 +145,28 @@ private function exportOrDownload($path, $function, callable $callback = null) $writer->close(); } + private function hasColumnsHeader() + { + return $this->columns_width && \count($this->columns_width) && is_string(array_keys($this->columns_width)[0]); + } + + private function customizeColumnsWidth(Writer $writer) + { + $data = $this->data->first(); + $keys = get_object_vars($data); + $columnsWithCount = count($this->columns_width); + if ($columnsWithCount === 0) { + return; + } + if ($columnsWithCount > 0 && $columnsWithCount < \count($keys)) { + throw new \Exception('The columns width elements count must match with header count.'); + } + $i = 1; + foreach ($this->columns_width as $width) { + $writer->setColumnsWidth($width, $i, $i++); + } + } + /** * Prepare collection by removing non string if required. */ @@ -177,3 +215,4 @@ public function headerStyle(Style $style) return $this; } } + diff --git a/src/Facades/FastExcel.php b/src/Facades/FastExcel.php index dd3e9a5..2c7e605 100644 --- a/src/Facades/FastExcel.php +++ b/src/Facades/FastExcel.php @@ -1,6 +1,6 @@ data = $data; + $this->sheet = $sheet; } /** diff --git a/src/Importable.php b/src/Importable.php index 5bd12cb..16d420d 100644 --- a/src/Importable.php +++ b/src/Importable.php @@ -1,6 +1,6 @@ assertInstanceOf(Collection::class, $collection); } - public function testIssue93() + public function testIssue93() { (new FastExcel($this->collection()))->export(__DIR__.'/猫.xlsx'); $this->assertTrue(file_exists(__DIR__.'/猫.xlsx')); From 3a16879582c89d0237e1115c078e64433f865160 Mon Sep 17 00:00:00 2001 From: Dariel Ramos Date: Wed, 25 Sep 2019 10:40:21 -0400 Subject: [PATCH 02/10] Comment code for transform always data (#2) --- src/Exportable.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Exportable.php b/src/Exportable.php index adf6c1d..c4d4ebb 100644 --- a/src/Exportable.php +++ b/src/Exportable.php @@ -184,9 +184,9 @@ protected function prepareCollection() $need_conversion = true; } } - if ($need_conversion) { + //if ($need_conversion) { $this->transform(); - } + //} } /** From e6b276e82ea6c7c45927ba1632b265786f61b37f Mon Sep 17 00:00:00 2001 From: Eliurkis Diaz Date: Fri, 27 Sep 2019 10:22:51 -0400 Subject: [PATCH 03/10] Update composer.json --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d672978..c98585b 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "php": "^7.0", "illuminate/database": "5.3.* || 5.4.* || 5.5.* || 5.6.* || 5.7.* || 5.8.*", "illuminate/support": "5.3.* || 5.4.* || 5.5.* || 5.6.* || 5.7.* || 5.8.*", - "smart145/spout": "^2.7.4" + "smart145/spout": "^2.7.6" }, "require-dev": { "phpunit/phpunit": "^6.4" From ea9e559f555e53473af08a9c656246bd55eec781 Mon Sep 17 00:00:00 2001 From: Eliurkis Diaz Date: Wed, 13 Nov 2019 13:17:32 -0500 Subject: [PATCH 04/10] Update composer.json --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index c98585b..d7fd028 100644 --- a/composer.json +++ b/composer.json @@ -10,9 +10,9 @@ ], "description": "Fast Excel import/export for Laravel", "require": { - "php": "^7.0", - "illuminate/database": "5.3.* || 5.4.* || 5.5.* || 5.6.* || 5.7.* || 5.8.*", - "illuminate/support": "5.3.* || 5.4.* || 5.5.* || 5.6.* || 5.7.* || 5.8.*", + "php": "7.*", + "illuminate/database": "5.* || 6.*", + "illuminate/support": "5.* || 6.*", "smart145/spout": "^2.7.6" }, "require-dev": { From 15c10ac0e57d592cbe883f4f8d7ecd6e0718fc99 Mon Sep 17 00:00:00 2001 From: Eliurkis Diaz Date: Fri, 15 Nov 2019 17:43:00 -0500 Subject: [PATCH 05/10] Update composer.json --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d7fd028..1772f78 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "php": "7.*", "illuminate/database": "5.* || 6.*", "illuminate/support": "5.* || 6.*", - "smart145/spout": "^2.7.6" + "smart145/spout": "2.7.*" }, "require-dev": { "phpunit/phpunit": "^6.4" From d35b67d44ccb372653fc1d7af7e77e406dcdb5f7 Mon Sep 17 00:00:00 2001 From: Eliurkis Diaz Date: Thu, 16 Apr 2020 00:15:12 -0400 Subject: [PATCH 06/10] Update composer.json --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 1772f78..f9696da 100644 --- a/composer.json +++ b/composer.json @@ -11,8 +11,8 @@ "description": "Fast Excel import/export for Laravel", "require": { "php": "7.*", - "illuminate/database": "5.* || 6.*", - "illuminate/support": "5.* || 6.*", + "illuminate/database": "5.* || 6.* || 7.*", + "illuminate/support": "5.* || 6.* || 7.*", "smart145/spout": "2.7.*" }, "require-dev": { From 24cc321d53502d04a94413013c29a5fca0f8a6c2 Mon Sep 17 00:00:00 2001 From: Eliurkis Diaz Date: Mon, 20 Apr 2020 15:06:23 -0400 Subject: [PATCH 07/10] Update composer.json --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index f9696da..2841120 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "php": "7.*", "illuminate/database": "5.* || 6.* || 7.*", "illuminate/support": "5.* || 6.* || 7.*", - "smart145/spout": "2.7.*" + "smart145/spout": "2.7.9" }, "require-dev": { "phpunit/phpunit": "^6.4" From 2eeb57672de80e27fbea7614879a65420072ec7e Mon Sep 17 00:00:00 2001 From: Eliurkis Diaz Date: Sun, 18 Oct 2020 14:17:23 -0400 Subject: [PATCH 08/10] Update composer.json --- composer.json | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/composer.json b/composer.json index 2841120..1bfeed8 100644 --- a/composer.json +++ b/composer.json @@ -11,8 +11,8 @@ "description": "Fast Excel import/export for Laravel", "require": { "php": "7.*", - "illuminate/database": "5.* || 6.* || 7.*", - "illuminate/support": "5.* || 6.* || 7.*", + "illuminate/database": "5.* || 6.* || 7.* || 8.*", + "illuminate/support": "5.* || 6.* || 7.* || 8.*", "smart145/spout": "2.7.9" }, "require-dev": { @@ -39,15 +39,5 @@ } }, "license": "MIT", - "authors": [ - { - "name": "rap2h", - "email": "raphaelht@gmail.com" - }, - { - "name": "Dariel Ramos", - "email": "vdariel90@gmail.com" - } - ], "minimum-stability": "stable" } From 51ee2d53a9aa0800cfdabdc621756ef55e2c83b7 Mon Sep 17 00:00:00 2001 From: Eli Diaz Date: Tue, 15 Nov 2022 23:14:44 -0500 Subject: [PATCH 09/10] Update to PHP 8.* --- composer.json | 8 ++++---- tests/FastExcelTest.php | 1 + tests/IssuesTest.php | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 1bfeed8..9da0a85 100644 --- a/composer.json +++ b/composer.json @@ -10,13 +10,13 @@ ], "description": "Fast Excel import/export for Laravel", "require": { - "php": "7.*", - "illuminate/database": "5.* || 6.* || 7.* || 8.*", - "illuminate/support": "5.* || 6.* || 7.* || 8.*", + "php": "8.*", + "illuminate/database": "5.* || 6.* || 7.* || 8.* || 9.*", + "illuminate/support": "5.* || 6.* || 7.* || 8.* || 9.*", "smart145/spout": "2.7.9" }, "require-dev": { - "phpunit/phpunit": "^6.4" + "phpunit/phpunit": "^9.5" }, "autoload": { "psr-4": { diff --git a/tests/FastExcelTest.php b/tests/FastExcelTest.php index 6a67eea..02456db 100644 --- a/tests/FastExcelTest.php +++ b/tests/FastExcelTest.php @@ -4,6 +4,7 @@ use Box\Spout\Writer\Style\Color; use Box\Spout\Writer\Style\StyleBuilder; +use PHPUnit\Framework\TestCase; use Smart145\FastExcel\FastExcel; use Smart145\FastExcel\SheetCollection; diff --git a/tests/IssuesTest.php b/tests/IssuesTest.php index c7688ab..442e777 100644 --- a/tests/IssuesTest.php +++ b/tests/IssuesTest.php @@ -5,6 +5,7 @@ use Box\Spout\Common\Type; use Box\Spout\Reader\ReaderFactory; use Illuminate\Support\Collection; +use PHPUnit\Framework\TestCase; use Smart145\FastExcel\FastExcel; use Smart145\FastExcel\SheetCollection; From 59ce628c38605bb58acebf738a50ce33e6c6a21d Mon Sep 17 00:00:00 2001 From: Josue Date: Tue, 25 Jul 2023 16:37:46 -0500 Subject: [PATCH 10/10] upgrade vendor[skip ci] --- .gitignore | 3 +- .scrutinizer.yml | 17 +- .travis.yml | 11 +- LICENSE | 2 +- README.md | 57 ++++- composer.json | 91 ++++--- phpunit.xml | 29 +-- src/Exportable.php | 278 ++++++++++++++------- src/Facades/FastExcel.php | 15 +- src/FastExcel.php | 160 ++++++++---- src/Importable.php | 125 +++++---- src/Providers/FastExcelServiceProvider.php | 2 +- src/SheetCollection.php | 2 +- src/functions/fastexcel.php | 10 +- tests/Dumb.php | 2 +- tests/FastExcelTest.php | 136 ++++++---- tests/IssuesTest.php | 125 +++++---- 17 files changed, 706 insertions(+), 359 deletions(-) diff --git a/.gitignore b/.gitignore index 3190c66..cee9142 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /vendor -/composer.lock /.idea /build +/composer.lock +.phpunit.result.cache diff --git a/.scrutinizer.yml b/.scrutinizer.yml index 516ca8b..463fed8 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -1,5 +1,15 @@ +build: + nodes: + analysis: + tests: + override: + - php-scrutinizer-run + filter: - excluded_paths: [tests, vendor] + excluded_paths: + - tests + dependency_paths: + - vendor checks: php: @@ -16,8 +26,3 @@ checks: fix_line_ending: true fix_identation_4spaces: true fix_doc_comments: true - -tools: - external_code_coverage: - timeout: 1800 - runs: 3 \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 8233d15..f346464 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,15 @@ language: php php: - - 7.1 + - 7.1.0 + - 7.2.0 + - 7.3.0 + - 7.4.0 + - 8.0.0 + - 8.1.0 install: - travis_retry composer install --no-interaction --prefer-source script: - vendor/bin/phpunit - -after_script: - - wget https://scrutinizer-ci.com/ocular.phar - - php ocular.phar code-coverage:upload --format=php-clover coverage.clover \ No newline at end of file diff --git a/LICENSE b/LICENSE index 69b27b0..380daf7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017 Raphaël Huchet (a.k.a rap2h / rap2hpoutre) +Copyright (c) 2017-present Raphaël Huchet (rap2h, rap2hpoutre) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 1baf894..415e0a3 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,8 @@ [![Version](https://poser.pugx.org/rap2hpoutre/fast-excel/version?format=flat)](https://packagist.org/packages/rap2hpoutre/fast-excel) [![License](https://poser.pugx.org/rap2hpoutre/fast-excel/license?format=flat)](https://packagist.org/packages/rap2hpoutre/fast-excel) -[![Build Status](https://travis-ci.org/rap2hpoutre/fast-excel.svg?branch=master)](https://travis-ci.org/rap2hpoutre/fast-excel) -[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/rap2hpoutre/fast-excel/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/rap2hpoutre/fast-excel/?branch=master) -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/4814d15bf1a545b99c90dc07917d7ec9)](https://www.codacy.com/app/rap2hpoutre/fast-excel?utm_source=github.com&utm_medium=referral&utm_content=rap2hpoutre/fast-excel&utm_campaign=Badge_Grade) +[![StyleCI](https://github.styleci.io/repos/128174809/shield?branch=master)](https://github.styleci.io/repos/128174809?branch=master) +[![Tests](https://github.com/rap2hpoutre/fast-excel/actions/workflows/tests.yml/badge.svg)](https://github.com/rap2hpoutre/fast-excel/actions/workflows/tests.yml) [![Total Downloads](https://poser.pugx.org/rap2hpoutre/fast-excel/downloads)](https://packagist.org/packages/rap2hpoutre/fast-excel) Fast Excel import/export for Laravel, thanks to [Spout](https://github.com/box/spout). @@ -82,7 +81,7 @@ $collection = (new FastExcel)->import('file.xlsx'); Import a `csv` with specific delimiter, enclosure characters and "gbk" encoding: ```php -$collection = (new FastExcel)->configureCsv(';', '#', '\n', 'gbk')->import('file.csv'); +$collection = (new FastExcel)->configureCsv(';', '#', 'gbk')->import('file.csv'); ``` Import and insert to database: @@ -160,14 +159,53 @@ You can also import a specific sheet by its number: $users = (new FastExcel)->sheet(3)->import('file.xlsx'); ``` +Import multiple sheets with sheets names: + +```php +$sheets = (new FastExcel)->withSheetsNames()->importSheets('file.xlsx'); +``` + +### Export large collections with chunk + +Export rows one by one to avoid `memory_limit` issues [using `yield`](https://www.php.net/manual/en/language.generators.syntax.php): + +```php +function usersGenerator() { + foreach (User::cursor() as $user) { + yield $user; + } +} + +// Export consumes only a few MB, even with 10M+ rows. +(new FastExcel(usersGenerator()))->export('test.xlsx'); +``` + +### Add header and rows style + +Add header and rows style with `headerStyle` and `rowsStyle` methods. + +```php +use OpenSpout\Common\Entity\Style\Style; + +$header_style = (new Style())->setFontBold(); + +$rows_style = (new Style()) + ->setFontSize(15) + ->setShouldWrapText() + ->setBackgroundColor("EDEDED"); + +return (new FastExcel($list)) + ->headerStyle($header_style) + ->rowsStyle($rows_style) + ->download('file.xlsx'); +``` + ## Why? FastExcel is intended at being Laravel-flavoured [Spout](https://github.com/box/spout): a simple, but elegant wrapper around [Spout](https://github.com/box/spout) with the goal -of simplifying **imports and exports**. - -It could be considered as a faster (and memory friendly) alternative -to [Laravel Excel](https://laravel-excel.maatwebsite.nl/), with less features. +of simplifying **imports and exports**. It could be considered as a faster (and memory friendly) alternative +to [Laravel Excel](https://laravel-excel.com/), with less features. Use it only for simple tasks. ## Benchmarks @@ -180,5 +218,4 @@ Testing a XLSX export for 10000 lines, 20 columns with random data, 10 iteration | Laravel Excel | 123.56 M | 11.56 s | | FastExcel | 2.09 M | 2.76 s | -Still, remember that [Laravel Excel](https://laravel-excel.maatwebsite.nl/) **has many more feature.** -Please help me improve benchmarks, more tests are coming. Feel free to criticize. +Still, remember that [Laravel Excel](https://laravel-excel.com/) **has many more features.** diff --git a/composer.json b/composer.json index 9da0a85..bde28b0 100644 --- a/composer.json +++ b/composer.json @@ -1,43 +1,54 @@ { - "name": "smart145/fast-excel", - "type": "library", - "keywords": [ - "laravel", - "excel", - "csv", - "xls", - "xlsx" - ], - "description": "Fast Excel import/export for Laravel", - "require": { - "php": "8.*", - "illuminate/database": "5.* || 6.* || 7.* || 8.* || 9.*", - "illuminate/support": "5.* || 6.* || 7.* || 8.* || 9.*", - "smart145/spout": "2.7.9" - }, - "require-dev": { - "phpunit/phpunit": "^9.5" - }, - "autoload": { - "psr-4": { - "Smart145\\FastExcel\\": "src/" + "name": "smart145/fast-excel", + "type": "library", + "keywords": [ + "laravel", + "excel", + "csv", + "xls", + "xlsx" + ], + "description": "Fast Excel import/export for Laravel", + "require": { + "php": "^8.0", + "illuminate/support": "^6.0 || ^7.0 || ^8.0 || ^9.0|^10.0", + "smart145/spout": "2.7.9" }, - "files": [ - "src/functions/fastexcel.php" - ] - }, - "autoload-dev": { - "psr-4": { - "Smart145\\FastExcel\\Tests\\": "tests/" - } - }, - "extra": { - "laravel": { - "providers": [ - "Smart145\\FastExcel\\Providers\\FastExcelServiceProvider" - ] - } - }, - "license": "MIT", - "minimum-stability": "stable" + "require-dev": { + "illuminate/database": "^6.20.12 || ^7.30.4 || ^8.24.0 || ^9.0|^10.0", + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "3.*" + }, + "autoload": { + "psr-4": { + "Smart145\\FastExcel\\": "src/" + }, + "files": [ + "src/functions/fastexcel.php" + ] + }, + "autoload-dev": { + "psr-4": { + "Smart145\\FastExcel\\Tests\\": "tests/" + } + }, + "extra": { + "laravel": { + "providers": [ + "Smart145\\FastExcel\\Providers\\FastExcelServiceProvider" + ] + } + }, + "scripts": { + "test": "vendor/bin/phpunit tests" + }, + "license": "MIT", + "authors": [ + { + "name": "rap2h", + "email": "raphaelht@gmail.com" + } + ], + "minimum-stability": "dev", + "prefer-stable": true } diff --git a/phpunit.xml b/phpunit.xml index 8ceda85..f661e62 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,20 +1,17 @@ - + - - tests/ + + ./tests - - - src/ - - - - - - - - - - \ No newline at end of file + + + + ./app + + + diff --git a/src/Exportable.php b/src/Exportable.php index c4d4ebb..4ae7e44 100644 --- a/src/Exportable.php +++ b/src/Exportable.php @@ -1,16 +1,21 @@ columns_width = $widths; - - return $this; - } - /** * @param $path - * @param string $function + * @param string $function * @param callable|null $callback * - * @throws \Box\Spout\Common\Exception\IOException - * @throws \Box\Spout\Common\Exception\InvalidArgumentException - * @throws \Box\Spout\Common\Exception\UnsupportedTypeException - * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException - * @throws \Box\Spout\Common\Exception\SpoutException + * @throws \OpenSpout\Common\Exception\IOException + * @throws \OpenSpout\Common\Exception\InvalidArgumentException + * @throws \OpenSpout\Common\Exception\UnsupportedTypeException + * @throws \OpenSpout\Writer\Exception\WriterNotOpenedException + * @throws \OpenSpout\Common\Exception\SpoutException */ private function exportOrDownload($path, $function, callable $callback = null) { - $writer = WriterFactory::create($this->getType($path)); - $this->setOptions($writer); - $this->customizeColumnsWidth($writer); - /* @var \Box\Spout\Writer\WriterInterface $writer */ - $writer->$function($path); - $writer->getCurrentSheet()->setName($this->sheet); + if (Str::endsWith($path, 'csv')) { + $options = new \OpenSpout\Writer\CSV\Options(); + $writer = new \OpenSpout\Writer\CSV\Writer($options); + } elseif (Str::endsWith($path, 'ods')) { + $options = new \OpenSpout\Writer\ODS\Options(); + $writer = new \OpenSpout\Writer\ODS\Writer($options); + } else { + $options = new \OpenSpout\Writer\XLSX\Options(); + $writer = new \OpenSpout\Writer\XLSX\Writer($options); + } + $this->setOptions($options); + /* @var \OpenSpout\Writer\WriterInterface $writer */ + $writer->$function($path); - $has_sheets = ($writer instanceof \Box\Spout\Writer\XLSX\Writer || $writer instanceof \Box\Spout\Writer\ODS\Writer); + $has_sheets = ($writer instanceof \OpenSpout\Writer\XLSX\Writer || $writer instanceof \OpenSpout\Writer\ODS\Writer); // It can export one sheet (Collection) or N sheets (SheetCollection) - $data = $this->data instanceof SheetCollection ? $this->data : collect([$this->data]); + $data = $this->transpose ? $this->transposeData() : ($this->data instanceof SheetCollection ? $this->data : collect([$this->data])); foreach ($data as $key => $collection) { if ($collection instanceof Collection) { - // Apply callback - if ($callback) { - $collection->transform(function ($value) use ($callback) { - return $callback($value); - }); - } - // Prepare collection (i.e remove non-string) - $this->prepareCollection(); - // Add header row. - if ($this->with_header) { - $first_row = $collection->first(); - $keys = $this->hasColumnsHeader() ? array_keys($this->columns_width) : array_keys(is_array($first_row) ? $first_row : $first_row->toArray()); - if ($this->header_style) { - $writer->addRowWithStyle($keys, $this->header_style); - } else { - $writer->addRow($keys); - } - } - $writer->addRows($collection->toArray()); + $this->writeRowsFromCollection($writer, $collection, $callback); + } elseif ($collection instanceof Generator) { + $this->writeRowsFromGenerator($writer, $collection, $callback); + } elseif (is_array($collection)) { + $this->writeRowsFromArray($writer, $collection, $callback); + } else { + throw new InvalidArgumentException('Unsupported type for $data'); } if (is_string($key)) { $writer->getCurrentSheet()->setName($key); @@ -145,35 +127,126 @@ private function exportOrDownload($path, $function, callable $callback = null) $writer->close(); } - private function hasColumnsHeader() + /** + * Transpose data from rows to columns. + * + * @return SheetCollection + */ + private function transposeData() { - return $this->columns_width && \count($this->columns_width) && is_string(array_keys($this->columns_width)[0]); + $data = $this->data instanceof SheetCollection ? $this->data : collect([$this->data]); + $transposedData = []; + + foreach ($data as $key => $collection) { + foreach ($collection as $row => $columns) { + foreach ($columns as $column => $value) { + data_set( + $transposedData, + implode('.', [ + $key, + $column, + $row, + ]), + $value + ); + } + } + } + + return new SheetCollection($transposedData); } - private function customizeColumnsWidth(Writer $writer) + private function writeRowsFromCollection($writer, Collection $collection, ?callable $callback = null) { - $data = $this->data->first(); - $keys = get_object_vars($data); - $columnsWithCount = count($this->columns_width); - if ($columnsWithCount === 0) { - return; + // Apply callback + if ($callback) { + $collection->transform(function ($value) use ($callback) { + return $callback($value); + }); } - if ($columnsWithCount > 0 && $columnsWithCount < \count($keys)) { - throw new \Exception('The columns width elements count must match with header count.'); + // Prepare collection (i.e remove non-string) + $this->prepareCollection($collection); + // Add header row. + if ($this->with_header) { + $this->writeHeader($writer, $collection->first()); } - $i = 1; - foreach ($this->columns_width as $width) { - $writer->setColumnsWidth($width, $i, $i++); + + // createRowFromArray works only with arrays + if (!is_array($collection->first())) { + $collection = $collection->map(function ($value) { + return $value->toArray(); + }); + } + + // is_array($first_row) ? $first_row : $first_row->toArray()) + $all_rows = $collection->map(function ($value) { + return Row::fromValues($value); + })->toArray(); + if ($this->rows_style) { + $this->addRowsWithStyle($writer, $all_rows, $this->rows_style); + } else { + $writer->addRows($all_rows); + } + } + + private function addRowsWithStyle($writer, $all_rows, $rows_style) + { + $styled_rows = []; + // Style rows one by one + foreach ($all_rows as $row) { + $styled_rows[] = Row::fromValues($row->toArray(), $rows_style); } + $writer->addRows($styled_rows); + } + + private function writeRowsFromGenerator($writer, Generator $generator, ?callable $callback = null) + { + foreach ($generator as $key => $item) { + // Apply callback + if ($callback) { + $item = $callback($item); + } + + // Prepare row (i.e remove non-string) + $item = $this->transformRow($item); + + // Add header row. + if ($this->with_header && $key === 0) { + $this->writeHeader($writer, $item); + } + // Write rows (one by one). + $writer->addRow(Row::fromValues($item->toArray(), $this->rows_style)); + } + } + + private function writeRowsFromArray($writer, array $array, ?callable $callback = null) + { + $collection = collect($array); + + if (is_object($collection->first()) || is_array($collection->first())) { + // provided $array was valid and could be converted to a collection + $this->writeRowsFromCollection($writer, $collection, $callback); + } + } + + private function writeHeader($writer, $first_row) + { + if ($first_row === null) { + return; + } + + $keys = array_keys(is_array($first_row) ? $first_row : $first_row->toArray()); + $writer->addRow(Row::fromValues($keys, $this->header_style)); +// $writer->addRow(WriterEntityFactory::createRowFromArray($keys, $this->header_style)); } /** * Prepare collection by removing non string if required. */ - protected function prepareCollection() + protected function prepareCollection(Collection $collection) { $need_conversion = false; - $first_row = $this->data->first(); + $first_row = $collection->first(); if (!$first_row) { return; @@ -184,22 +257,30 @@ protected function prepareCollection() $need_conversion = true; } } - //if ($need_conversion) { - $this->transform(); - //} + if ($need_conversion) { + $this->transform($collection); + } } /** * Transform the collection. */ - private function transform() + private function transform(Collection $collection) { - $this->data->transform(function ($data) { - return collect($data)->map(function ($value) { - return is_int($value) || is_float($value) || is_null($value) ? (string) $value : $value; - })->filter(function ($value) { - return is_string($value); - }); + $collection->transform(function ($data) { + return $this->transformRow($data); + }); + } + + /** + * Transform one row (i.e remove non-string). + */ + private function transformRow($data) + { + return collect($data)->map(function ($value) { + return is_null($value) ? (string) $value : $value; + })->filter(function ($value) { + return is_string($value) || is_int($value) || is_float($value); }); } @@ -214,5 +295,16 @@ public function headerStyle(Style $style) return $this; } -} + /** + * @param Style $style + * + * @return Exportable + */ + public function rowsStyle(Style $style) + { + $this->rows_style = $style; + + return $this; + } +} diff --git a/src/Facades/FastExcel.php b/src/Facades/FastExcel.php index 2c7e605..3001fa3 100644 --- a/src/Facades/FastExcel.php +++ b/src/Facades/FastExcel.php @@ -1,9 +1,22 @@ ',', 'enclosure' => '"', - 'eol' => "\n", 'encoding' => 'UTF-8', 'bom' => true, ]; + /** + * @var callable + */ + protected $reader_configurator = null; + + /** + * @var callable + */ + protected $writer_configurator = null; + /** * FastExcel constructor. * - * @param Collection $data + * @param array|Generator|Collection|null $data */ - public function __construct($data = null, $sheet = 'Sheet1') + public function __construct(array|Generator|Collection $data = null) { $this->data = $data; - $this->sheet = $sheet; } /** * Manually set data apart from the constructor. * - * @param Collection $data + * @param Collection|Generator|array $data * * @return FastExcel */ @@ -66,22 +88,6 @@ public function data($data) return $this; } - /** - * @param $path - * - * @return string - */ - protected function getType($path) - { - if (Str::endsWith($path, Type::CSV)) { - return Type::CSV; - } elseif (Str::endsWith($path, Type::ODS)) { - return Type::ODS; - } else { - return Type::XLSX; - } - } - /** * @param $sheet_number * @@ -104,37 +110,105 @@ public function withoutHeaders() return $this; } + /** + * @return $this + */ + public function withSheetsNames() + { + $this->with_sheets_names = true; + + return $this; + } + + /** + * @return $this + */ + public function startRow(int $row) + { + $this->start_row = $row; + + return $this; + } + + /** + * @return $this + */ + public function transpose() + { + $this->transpose = true; + + return $this; + } + /** * @param string $delimiter * @param string $enclosure - * @param string $eol * @param string $encoding * @param bool $bom * * @return $this */ - public function configureCsv($delimiter = ',', $enclosure = '"', $eol = "\n", $encoding = 'UTF-8', $bom = false) + public function configureCsv($delimiter = ',', $enclosure = '"', $encoding = 'UTF-8', $bom = false) + { + $this->csv_configuration = compact('delimiter', 'enclosure', 'encoding', 'bom'); + + return $this; + } + + /** + * Configure the underlying Spout Reader using a callback. + * + * @param callable|null $callback + * + * @return $this + */ + public function configureReaderUsing(?callable $callback = null) { - $this->csv_configuration = compact('delimiter', 'enclosure', 'eol', 'encoding', 'bom'); + $this->reader_configurator = $callback; return $this; } /** - * @param \Box\Spout\Reader\ReaderInterface|\Box\Spout\Writer\WriterInterface $reader_or_writer + * Configure the underlying Spout Reader using a callback. + * + * @param callable|null $callback + * + * @return $this + */ + public function configureWriterUsing(?callable $callback = null) + { + $this->writer_configurator = $callback; + + return $this; + } + + /** + * @param \OpenSpout\Reader\ReaderInterface|\OpenSpout\Writer\WriterInterface $reader_or_writer */ protected function setOptions(&$reader_or_writer) { - if ($reader_or_writer instanceof CSVReader || $reader_or_writer instanceof CSVWriter) { - $reader_or_writer->setFieldDelimiter($this->csv_configuration['delimiter']); - $reader_or_writer->setFieldEnclosure($this->csv_configuration['enclosure']); - if ($reader_or_writer instanceof CSVReader) { - $reader_or_writer->setEndOfLineCharacter($this->csv_configuration['eol']); - $reader_or_writer->setEncoding($this->csv_configuration['encoding']); + if ($reader_or_writer instanceof CsvReaderOptions || $reader_or_writer instanceof CsvWriterOptions) { + $reader_or_writer->FIELD_DELIMITER = $this->csv_configuration['delimiter']; + $reader_or_writer->FIELD_ENCLOSURE = $this->csv_configuration['enclosure']; + if ($reader_or_writer instanceof CsvReaderOptions) { + $reader_or_writer->ENCODING = $this->csv_configuration['encoding']; } - if ($reader_or_writer instanceof CSVWriter) { - $reader_or_writer->setShouldAddBOM($this->csv_configuration['bom']); + if ($reader_or_writer instanceof CsvWriterOptions) { + $reader_or_writer->SHOULD_ADD_BOM = $this->csv_configuration['bom']; } } + + if ($reader_or_writer instanceof ReaderInterface && is_callable($this->reader_configurator)) { + call_user_func( + $this->reader_configurator, + $reader_or_writer + ); + } elseif ($reader_or_writer instanceof WriterInterface && is_callable($this->writer_configurator)) { + call_user_func( + $this->writer_configurator, + $reader_or_writer + ); + } } } diff --git a/src/Importable.php b/src/Importable.php index 16d420d..7cc240e 100644 --- a/src/Importable.php +++ b/src/Importable.php @@ -1,14 +1,16 @@ getSheetIterator() as $key => $sheet) { - $collections[] = $this->importSheet($sheet, $callback); + if ($this->with_sheets_names) { + $collections[$sheet->getName()] = $this->importSheet($sheet, $callback); + } else { + $collections[] = $this->importSheet($sheet, $callback); + } } $reader->close(); @@ -83,21 +82,58 @@ public function importSheets($path, callable $callback = null) /** * @param $path * - * @throws \Box\Spout\Common\Exception\IOException - * @throws \Box\Spout\Common\Exception\UnsupportedTypeException + * @throws \OpenSpout\Common\Exception\UnsupportedTypeException + * @throws \OpenSpout\Common\Exception\IOException * - * @return \Box\Spout\Reader\ReaderInterface + * @return \OpenSpout\Reader\ReaderInterface */ private function reader($path) { - $reader = ReaderFactory::create($this->getType($path)); - $this->setOptions($reader); - /* @var \Box\Spout\Reader\ReaderInterface $reader */ + if (Str::endsWith($path, 'csv')) { + $options = new \OpenSpout\Reader\CSV\Options(); + $this->setOptions($options); + $reader = new \OpenSpout\Reader\CSV\Reader($options); + } elseif (Str::endsWith($path, 'ods')) { + $options = new \OpenSpout\Reader\ODS\Options(); + $this->setOptions($options); + $reader = new \OpenSpout\Reader\ODS\Reader($options); + } else { + $options = new \OpenSpout\Reader\XLSX\Options(); + $this->setOptions($options); + $reader = new \OpenSpout\Reader\XLSX\Reader($options); + } + + /* @var \OpenSpout\Reader\ReaderInterface $reader */ $reader->open($path); return $reader; } + /** + * @param array $array + * + * @return array + */ + private function transposeCollection(array $array) + { + $collection = []; + + foreach ($array as $row => $columns) { + foreach ($columns as $column => $value) { + data_set( + $collection, + implode('.', [ + $column, + $row, + ]), + $value + ); + } + } + + return $collection; + } + /** * @param SheetInterface $sheet * @param callable|null $callback @@ -110,30 +146,33 @@ private function importSheet(SheetInterface $sheet, callable $callback = null) $collection = []; $count_header = 0; - if ($this->with_header) { - foreach ($sheet->getRowIterator() as $k => $row) { - if ($k == 1) { - $headers = $this->toStrings($row); - $count_header = count($headers); - continue; - } - if ($count_header > $count_row = count($row)) { - $row = array_merge($row, array_fill(0, $count_header - $count_row, null)); - } elseif ($count_header < $count_row = count($row)) { - $row = array_slice($row, 0, $count_header); + foreach ($sheet->getRowIterator() as $k => $rowAsObject) { + $row = $rowAsObject->toArray(); + if ($k >= $this->start_row) { + if ($this->with_header) { + if ($k == $this->start_row) { + $headers = $this->toStrings($row); + $count_header = count($headers); + continue; + } + if ($count_header > $count_row = count($row)) { + $row = array_merge($row, array_fill(0, $count_header - $count_row, null)); + } elseif ($count_header < $count_row = count($row)) { + $row = array_slice($row, 0, $count_header); + } } if ($callback) { - if ($result = $callback(array_combine($headers, $row))) { + if ($result = $callback(empty($headers) ? $row : array_combine($headers, $row))) { $collection[] = $result; } } else { - $collection[] = array_combine($headers, $row); + $collection[] = empty($headers) ? $row : array_combine($headers, $row); } } - } else { - foreach ($sheet->getRowIterator() as $row) { - $collection[] = $row; - } + } + + if ($this->transpose) { + return $this->transposeCollection($collection); } return $collection; @@ -147,7 +186,9 @@ private function importSheet(SheetInterface $sheet, callable $callback = null) private function toStrings($values) { foreach ($values as &$value) { - if ($value instanceof \Datetime) { + if ($value instanceof \DateTime) { + $value = $value->format('Y-m-d H:i:s'); + } elseif ($value instanceof \DateTimeImmutable) { $value = $value->format('Y-m-d H:i:s'); } elseif ($value) { $value = (string) $value; diff --git a/src/Providers/FastExcelServiceProvider.php b/src/Providers/FastExcelServiceProvider.php index 79ed812..3a89308 100644 --- a/src/Providers/FastExcelServiceProvider.php +++ b/src/Providers/FastExcelServiceProvider.php @@ -1,6 +1,6 @@ make('fastexcel')->data($data); + } + if (is_object($data) && method_exists($data, 'toArray')) { $data = $data->toArray(); } - return blank($data) ? app()->make('fastexcel') : app()->makeWith('fastexcel', $data); + return $data === null ? app()->make('fastexcel') : app()->makeWith('fastexcel', $data); } } diff --git a/tests/Dumb.php b/tests/Dumb.php index b708d3c..6e534c7 100644 --- a/tests/Dumb.php +++ b/tests/Dumb.php @@ -1,6 +1,6 @@ collect([['test' => 'row1 col1'], ['test' => 'row2 col1'], ['test' => 'row3 col1']]), + 'Sheet with name B' => $this->collection(), + ]; + $file = __DIR__.'/test_multi_sheets_with_sheets_names.xlsx'; + $sheets = new SheetCollection($collections); + (new FastExcel($sheets))->export($file); + + $sheets = (new FastExcel())->withSheetsNames()->importSheets($file); + $this->assertInstanceOf(SheetCollection::class, $sheets); + + $this->assertEquals($collections['Sheet with name A'], collect($sheets->get('Sheet with name A'))); + $this->assertEquals($collections['Sheet with name B'], collect($sheets->get('Sheet with name B'))); + + unlink($file); + } + + /** + * @throws \OpenSpout\Common\Exception\IOException + * @throws \OpenSpout\Common\Exception\InvalidArgumentException + * @throws \OpenSpout\Common\Exception\UnsupportedTypeException + * @throws \OpenSpout\Reader\Exception\ReaderNotOpenedException + * @throws \OpenSpout\Writer\Exception\WriterNotOpenedException */ public function testExportWithHeaderStyle() { $original_collection = $this->collection(); - $style = (new StyleBuilder()) - ->setFontBold() - ->setBackgroundColor(Color::YELLOW) - ->build(); + + $style = new Style(); + $style->setFontBold(); + $style->setFontSize(15); + $style->setFontColor(Color::BLUE); + $style->setShouldWrapText(); + $style->setBackgroundColor(Color::YELLOW); $file = __DIR__.'/test-header-style.xlsx'; (new FastExcel(clone $original_collection)) ->headerStyle($style) diff --git a/tests/IssuesTest.php b/tests/IssuesTest.php index 442e777..7c94277 100644 --- a/tests/IssuesTest.php +++ b/tests/IssuesTest.php @@ -1,13 +1,10 @@ collection()))->export('test2.xlsx'); - $this->assertEquals(__DIR__.'/test2.xlsx', $path); + $this->assertEquals(__DIR__.DIRECTORY_SEPARATOR.'test2.xlsx', $path); unlink($path); } /** - * @throws \Box\Spout\Common\Exception\IOException - * @throws \Box\Spout\Common\Exception\InvalidArgumentException - * @throws \Box\Spout\Common\Exception\UnsupportedTypeException - * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException - * @throws \Box\Spout\Reader\Exception\ReaderNotOpenedException + * @throws \OpenSpout\Common\Exception\IOException + * @throws \OpenSpout\Common\Exception\InvalidArgumentException + * @throws \OpenSpout\Common\Exception\UnsupportedTypeException + * @throws \OpenSpout\Writer\Exception\WriterNotOpenedException + * @throws \OpenSpout\Reader\Exception\ReaderNotOpenedException */ public function testIssue19() { @@ -74,16 +71,16 @@ public function testIssue19() } /** - * @throws \Box\Spout\Common\Exception\IOException - * @throws \Box\Spout\Common\Exception\InvalidArgumentException - * @throws \Box\Spout\Common\Exception\UnsupportedTypeException - * @throws \Box\Spout\Reader\Exception\ReaderNotOpenedException - * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException + * @throws \OpenSpout\Common\Exception\IOException + * @throws \OpenSpout\Common\Exception\InvalidArgumentException + * @throws \OpenSpout\Common\Exception\UnsupportedTypeException + * @throws \OpenSpout\Reader\Exception\ReaderNotOpenedException + * @throws \OpenSpout\Writer\Exception\WriterNotOpenedException */ public function testIssue26() { chdir(__DIR__); - foreach ([[[]], null, [null]] as $value) { + foreach ([[[]], [null]] as $value) { $path = (new FastExcel($value))->export('test2.xlsx'); $this->assertEquals(collect([]), (new FastExcel())->import(__DIR__.'/test2.xlsx')); unlink($path); @@ -91,11 +88,11 @@ public function testIssue26() } /** - * @throws \Box\Spout\Common\Exception\IOException - * @throws \Box\Spout\Common\Exception\InvalidArgumentException - * @throws \Box\Spout\Common\Exception\UnsupportedTypeException - * @throws \Box\Spout\Reader\Exception\ReaderNotOpenedException - * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException + * @throws \OpenSpout\Common\Exception\IOException + * @throws \OpenSpout\Common\Exception\InvalidArgumentException + * @throws \OpenSpout\Common\Exception\UnsupportedTypeException + * @throws \OpenSpout\Reader\Exception\ReaderNotOpenedException + * @throws \OpenSpout\Writer\Exception\WriterNotOpenedException */ public function testIssue32() { @@ -116,17 +113,19 @@ public function testIssue32() } /** - * @throws \Box\Spout\Common\Exception\IOException - * @throws \Box\Spout\Common\Exception\InvalidArgumentException - * @throws \Box\Spout\Common\Exception\UnsupportedTypeException - * @throws \Box\Spout\Reader\Exception\ReaderNotOpenedException - * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException + * @throws \OpenSpout\Common\Exception\IOException + * @throws \OpenSpout\Common\Exception\InvalidArgumentException + * @throws \OpenSpout\Common\Exception\UnsupportedTypeException + * @throws \OpenSpout\Reader\Exception\ReaderNotOpenedException + * @throws \OpenSpout\Writer\Exception\WriterNotOpenedException */ public function testIssue40() { $col = new SheetCollection(['1st Sheet' => $this->collection(), '2nd Sheet' => $this->collection()]); (new FastExcel($col))->export(__DIR__.'/test2.xlsx'); - $reader = ReaderFactory::create(Type::XLSX); + + $options = new \OpenSpout\Reader\XLSX\Options(); + $reader = new \OpenSpout\Reader\XLSX\Reader($options); $reader->open(__DIR__.'/test2.xlsx'); foreach ($reader->getSheetIterator() as $key => $sheet) { $this->assertEquals($sheet->getName(), $key === 2 ? '2nd Sheet' : '1st Sheet'); @@ -147,4 +146,46 @@ public function testIssue93() $this->assertTrue(file_exists(__DIR__.'/猫.xlsx')); unlink(__DIR__.'/猫.xlsx'); } + + public function testIssue86() + { + $users = (new FastExcel())->withoutHeaders()->import(__DIR__.'/test1.xlsx', function ($line) { + return $line; + }); + $this->assertCount(4, $users); + $this->assertEquals($users[0], ['col1', 'col2']); + } + + public function testIssue104() + { + $users = (new FastExcel())->import(__DIR__.'/test104.xlsx', function ($line) { + return $line; + }); + $this->assertCount(3, $users); + $this->assertEquals($users[0], [ + 'Name' => 'joe', + 'Email' => 'joe@gmail.com', + 'Password' => 'asdadasdasdasdasd', + ]); + } + + public function testIssue310() + { + $original_collection = $this->collection(); + $delimiter = ';'; + $file = 'issue_310.csv'; + + (new FastExcel(clone $original_collection)) + ->configureCsv($delimiter) + ->export($file); + + $this->assertEquals( + $original_collection, + (new FastExcel()) + ->configureCsv($delimiter) + ->import($file) + ); + + unlink($file); + } }