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); + } }