diff --git a/.gitattributes b/.gitattributes index 1d15a0644..495d713ba 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,4 @@ -* text=auto +* text eol=lf /.github export-ignore /tests export-ignore diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index d9adcd679..4303b150a 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,6 +1,6 @@ + * PHP version: * Laravel version: * Package version: @@ -36,7 +38,7 @@ Put an X between the brackets if you have done the following: ### Steps to Reproduce - + **Expected behavior:** diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md new file mode 100644 index 000000000..736ea24e7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -0,0 +1,60 @@ +--- +name: 馃悰 Bug Report +about: If something isn't working as expected 馃. + +--- + + + +### Prerequisites + + + +* [ ] Able to reproduce the behaviour outside of your code, the problem is isolated to Laravel Excel. +* [ ] Checked that your issue isn't already filed. +* [ ] Checked if no PR was submitted that fixes this problem. + +### Versions + + + +* PHP version: +* Laravel version: +* Package version: + +### Description + + + +### Steps to Reproduce + + + +**Expected behavior:** + + + +**Actual behavior:** + + + +### Additional Information + +Any additional information, configuration or data that might be necessary to reproduce the issue. + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 02f00b250..49178d005 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -5,10 +5,10 @@ Filling out the template is required. Any pull request that does not include eno Mark the following tasks as done: -* [ ] Check the codebase to ensure that your feature doesn't already exist. -* [ ] Check the pull requests to ensure that another person hasn't already submitted the feature or fix. -* [ ] Documentation was adjusted -* [ ] Tests were added to ensure against regression. +* [ ] Checked the codebase to ensure that your feature doesn't already exist. +* [ ] Checked the pull requests to ensure that another person hasn't already submitted the feature or fix. +* [ ] Adjusted the Documentation. +* [ ] Added tests to ensure against regression. ### Description of the Change @@ -48,4 +48,4 @@ What process did you follow to verify that your change has the desired effects? ### Applicable Issues - \ No newline at end of file + diff --git a/.github/issuecomplete.yml b/.github/issuecomplete.yml new file mode 100644 index 000000000..395ba5368 --- /dev/null +++ b/.github/issuecomplete.yml @@ -0,0 +1,15 @@ +# The name of the label to apply when an issue does not have all tasks checked +labelName: more information needed + +# The text of the comment to add to the issue in addition to the label +commentText: > + Thanks for submitting the ticket. Unfortunately the information you provided is incomplete. We need to know which version you use and how to reproduce it. Please include code examples. Before we can pick it up, please check (https://github.com/Maatwebsite/Laravel-Excel/blob/3.0/.github/ISSUE_TEMPLATE.md) and add the missing information. + To make processing of this ticket a lot easier, please make sure to check (https://laravel-excel.maatwebsite.nl/docs/3.0/getting-started/contributing) and double-check if you have filled in the issue template correctly. This will allow us to pick up your ticket more efficiently. Issues that follow the guidelines correctly will get priority over other issues. + +# Whether or not to ensure all checkboxes are checked +checkCheckboxes: false + +# Keywords to look for in the body of the issue +keywords: + - Steps to Reproduce + - Versions diff --git a/.gitignore b/.gitignore index 5c990e362..3e2838368 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ composer.phar composer.lock .DS_Store .idea -phpunit.xml \ No newline at end of file +phpunit.xml +.phpunit.result.cache \ No newline at end of file diff --git a/.styleci.yml b/.styleci.yml index 6f7564504..12c482dd3 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -11,4 +11,10 @@ enabled: disabled: - concat_without_spaces - not_operator_with_successor_space - - unalign_equals \ No newline at end of file + - unalign_equals + +finder: + not-name: + - "*.md" + not-path: + - ".github" \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 52992133f..2eaf22e5a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,29 +14,34 @@ matrix: include: # Laravel 5.5 - php: 7.0 - env: LARAVEL=5.5.* TESTBENCH=3.5.* PHPUNIT=~6.0 COVERAGE=0 + env: LARAVEL=5.5.* TESTBENCH=3.5.* ORCHESTRA_DATABASE=3.5.* PHPUNIT=~6.0 - php: 7.1 - env: LARAVEL=5.5.* TESTBENCH=3.5.* PHPUNIT=~6.0 COVERAGE=0 + env: LARAVEL=5.5.* TESTBENCH=3.5.* ORCHESTRA_DATABASE=3.5.* PHPUNIT=~6.0 - php: 7.2 - env: LARAVEL=5.5.* TESTBENCH=3.5.* PHPUNIT=~6.0 COVERAGE=0 + env: LARAVEL=5.5.* TESTBENCH=3.5.* ORCHESTRA_DATABASE=3.5.* PHPUNIT=~6.0 # Laravel 5.6 - php: 7.1 - env: LARAVEL=5.6.* TESTBENCH=3.6.* PHPUNIT=~7.0 COVERAGE=0 + env: LARAVEL=5.6.* TESTBENCH=3.6.* ORCHESTRA_DATABASE=3.6.* PHPUNIT=~7.0 - php: 7.2 - env: LARAVEL=5.6.* TESTBENCH=3.6.* PHPUNIT=~7.0 COVERAGE=1 + env: LARAVEL=5.6.* TESTBENCH=3.6.* ORCHESTRA_DATABASE=3.6.* PHPUNIT=~7.0 + + # Laravel 5.7 + - php: 7.1 + env: LARAVEL=5.7.* TESTBENCH=3.7.* ORCHESTRA_DATABASE=3.7.*@dev PHPUNIT=~7.0 + - php: 7.2 + env: LARAVEL=5.7.* TESTBENCH=3.7.* ORCHESTRA_DATABASE=3.7.*@dev PHPUNIT=~7.0 before_install: - composer self-update --stable --no-interaction - - composer require orchestra/testbench:$TESTBENCH phpunit/phpunit:$PHPUNIT --no-update --no-interaction --dev + - composer require orchestra/testbench:$TESTBENCH orchestra/database:$ORCHESTRA_DATABASE phpunit/phpunit:$PHPUNIT --no-update --no-interaction --dev - mysql -e 'CREATE DATABASE IF NOT EXISTS laravel_excel;' install: - travis_retry composer install --no-suggest --no-interaction script: - - if [ "$COVERAGE" == "1" ]; then vendor/bin/phpunit --verbose --configuration phpunit.xml.dist --coverage-clover=coverage.xml; fi - - if [ "$COVERAGE" == "0" ]; then vendor/bin/phpunit --verbose --configuration phpunit.xml.dist; fi + - vendor/bin/phpunit --verbose --configuration phpunit.xml.dist after_success: - bash <(curl -s https://codecov.io/bash) \ No newline at end of file diff --git a/README.md b/README.md index e2e1defa8..d5d01e740 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,147 @@ -![Laravel-Excel 3.0](https://user-images.githubusercontent.com/7728097/37357058-b96e8d98-26e7-11e8-94a7-68f1f6fab99a.jpg) +

+ + Laravel Excel + +

+ +

+ Laravel Excel 3.0 +

+ +

+ :muscle: :fire: :rocket: +

+ +

+ Supercharged Excel exports
+ A simple, but elegant wrapper around PhpSpreadsheet with the goal of simplifying +exports. +

+ +

+ + Build Status + + + + StyleCI + + + + Latest Stable Version + + + + Total Downloads + + + + License + +

+ +

+ Quickstart + + Documentation + + Nova + + Blog + + Contributing + + Support +

+ +- **Easily export collections to Excel.** Supercharge your Laravel collections and export them directly to an Excel or CSV document. Exporting has never been so easy. + +- **Supercharged exports.** Export queries with automatic chunking for better peformance. You provide us the query, we handle the performance. Exporting even larger datasets? No worries, Laravel Excel has your back. You can queue your exports so all of this happens in the background. + +- **Export blade views.** Want to have a custom layout in your spreadsheet? Use a HTML table in a blade view and export that to Excel. + +## :rocket: 5 minutes quick start + +:bulb: Require this package in the `composer.json` of your Laravel project. This will download the package and PhpSpreadsheet. -# Laravel Excel 3.0 +``` +composer require maatwebsite/excel +``` -[![Build Status](https://travis-ci.org/Maatwebsite/Laravel-Excel.svg?branch=3.0)](https://travis-ci.org/Maatwebsite/Laravel-Excel) -[![codecov](https://codecov.io/gh/Maatwebsite/Laravel-Excel/branch/3.0/graph/badge.svg)](https://codecov.io/gh/Maatwebsite/Laravel-Excel) -[![StyleCI](https://styleci.io/repos/14259390/shield?branch=3.0)](https://styleci.io/repos/14259390) -[![Latest Stable Version](https://poser.pugx.org/maatwebsite/excel/v/stable.png)](https://packagist.org/packages/maatwebsite/excel) -[![Total Downloads](https://poser.pugx.org/maatwebsite/excel/downloads.png)](https://packagist.org/packages/maatwebsite/excel) -[![License](https://poser.pugx.org/maatwebsite/excel/license.png)](https://packagist.org/packages/maatwebsite/excel) +:muscle: Create an export class in `app/Exports` -## Introduction +``` +php artisan make:export UsersExport --model=User +``` -Laravel Excel is intended at being Laravel-flavoured PhpSpreadsheet: a simple, but elegant wrapper around PhpSpreadsheet with the goal of simplifying -exports. +This should have created: -[PhpSpreadsheet](https://phpspreadsheet.readthedocs.io/) is a library written in pure PHP and providing a set of classes that allow you to read from and to write to different spreadsheet file formats, like Excel and LibreOffice Calc. +```php + Excel::HTML, 'html' => Excel::HTML, 'csv' => Excel::CSV, + 'tsv' => Excel::TSV, /* |-------------------------------------------------------------------------- diff --git a/docs/export.md b/docs/export.md deleted file mode 100644 index 33df92969..000000000 --- a/docs/export.md +++ /dev/null @@ -1,12 +0,0 @@ -@include:Basics|basics -@include:Storing files|store -@include:Exportables|exportables -@include:From Query|from-query -@include:From View|from-view -@include:Queued|queued -@include:Multiple Sheets|multiple-sheets -@include:Mapping data|mapping -@include:Column Formatting|column-formatting -@include:Concerns overview|concerns -@include:Extending|extending -@include:Testing|testing \ No newline at end of file diff --git a/docs/export/basics.md b/docs/export/basics.md deleted file mode 100644 index 4fd4bcae8..000000000 --- a/docs/export/basics.md +++ /dev/null @@ -1,79 +0,0 @@ -# Basics - -The easiest way to start an export is by creating a custom export class. We'll use an invoices export as example. - -Create a new class called `InvoicesExport` in `App/Exports`: - -```php -namespace App\Exports; - -class InvoicesExport implements FromCollection -{ - public function collection() - { - return Invoice::all(); - } -} -``` - -In your controller we can now download this export. - -```php -public function export() -{ - return Excel::download(new InvoicesExport, 'invoices.xlsx'); -} -``` - -Or store it on a (e.g. s3) disk: - -```php -public function storeExcel() -{ - return Excel::store(new InvoicesExport, 'invoices.xlsx', 's3'); -} -``` - -### Dependency injection - -In case your export needs dependencies, you can inject the export class: - -```php -namespace App\Exports; - -class InvoicesExport implements FromCollection -{ - public function __construct(InvoicesRepository $invoices) - { - $this->invoices = $invoices; - } - - public function collection() - { - return $this->invoices->all(); - } -} -``` - -```php -public function export(Excel $excel, InvoicesExport $export) -{ - return $excel->download($export, 'invoices.xlsx'); -} -``` - -### Collection macros - -The package provides some macro to Laravel's collection class to easily download or store a collection. - -#### Downloading a collection as Excel - -```php -(new Collection([[1, 2, 3], [1, 2, 3]))->downloadExcel($filePath, $writerType = null) -``` - -#### Storing a collection on disk - -```php -(new Collection([[1, 2, 3], [1, 2, 3]))->storeExcel($filePath, $disk = null, $writerType = null) -``` \ No newline at end of file diff --git a/docs/export/column-formatting.md b/docs/export/column-formatting.md deleted file mode 100644 index ce99c5464..000000000 --- a/docs/export/column-formatting.md +++ /dev/null @@ -1,38 +0,0 @@ -# Formatting columns - -You can easily format an entire column, by using `WithColumnFormatting`. -In case you want something more complicated, it's suggested to use the `AfterSheet` event to directly interact with the underlying `Worksheet` class. - -```php - -use PhpOffice\PhpSpreadsheet\Shared\Date; -use PhpOffice\PhpSpreadsheet\Style\NumberFormat; - -class InvoicesExport implements WithColumnFormatting, WithMapping -{ - public function map($invoice): array - { - return [ - $invoice->invoice_number, - Date::dateTimeToExcel($invoice->created_at), - $invoice->total - ]; - } - - /** - * @return array - */ - public function columnFormats(): array - { - return [ - 'B' => NumberFormat::FORMAT_DATE_DDMMYYYY, - 'C' => NumberFormat::FORMAT_CURRENCY_EUR_SIMPLE, - ]; - } -} -``` - -### Dates - -In case of working with dates, it's recommended to use `\PhpOffice\PhpSpreadsheet\Shared\Date::dateTimeToExcel()` in your mapping -to ensure correct parsing of dates. \ No newline at end of file diff --git a/docs/export/concerns.md b/docs/export/concerns.md deleted file mode 100644 index 5555a3634..000000000 --- a/docs/export/concerns.md +++ /dev/null @@ -1,21 +0,0 @@ -# Export concerns - -| Interface | Explanation | -|---- |----| -|`Maatwebsite\Excel\Concerns\FromCollection`| Use a Laravel Collection to populate the export. | -|`Maatwebsite\Excel\Concerns\FromQuery`| Use an Eloquent query to populate the export. | -|`Maatwebsite\Excel\Concerns\FromView`| Use a (Blade) view to to populate the export. | -|`Maatwebsite\Excel\Concerns\WithTitle`| Set the Workbook or Worksheet title. | -|`Maatwebsite\Excel\Concerns\WithHeadings`| Prepend given heading row. | -|`Maatwebsite\Excel\Concerns\WithMapping`| Format the row before it's written to the file. | -|`Maatwebsite\Excel\Concerns\WithColumnFormatting`| Format certain columns. | -|`Maatwebsite\Excel\Concerns\WithMultipleSheets`| Enable multi-sheet support. Each sheet can have its own concerns (except this one). | -|`Maatwebsite\Excel\Concerns\ShouldAutoSize`| Auto-size the columns in the worksheet. | -|`Maatwebsite\Excel\Concerns\WithEvents`| Register events to hook into the PhpSpreadsheet process. | - -### Traits - -| Trait | Explanation | -|---- |----| -|`Maatwebsite\Excel\Concerns\Exportable` | Add download/store abilities right on the export class itself. -|`Maatwebsite\Excel\Concerns\RegistersEventListeners` | Auto-register the available event listeners. | diff --git a/docs/export/exportables.md b/docs/export/exportables.md deleted file mode 100644 index d39f904cd..000000000 --- a/docs/export/exportables.md +++ /dev/null @@ -1,61 +0,0 @@ -# Exportables - -In the previous example, we used the `Excel::download` facade to start an export. - -Laravel-Excel also provides a `Maatwebsite\Excel\Concerns\Exportable` trait, to make export classes exportable. - -```php -namespace App\Exports; - -class InvoicesExport implements FromCollection -{ - use Exportable; - - public function collection() - { - return Invoice::all(); - } -} -``` - -We can now download the export without the need for the facade: - -```php -return (new InvoicesExport)->download('invoices.xlsx'); -``` - -Or store it on a disk: - -```php -return (new InvoicesExport)->store('invoices.xlsx', 's3'); -``` - -### Responsable - -The previous example can be made even shorter. Add Laravel's `Responsable` interface to the export class. - -```php -namespace App\Exports; - -class InvoicesExport implements FromCollection, Responsable -{ - use Exportable; - - /** - * It's required to define the fileName within - * the export class when making use of Responsable. - */ - private $fileName = 'invoices.xlsx'; - - public function collection() - { - return Invoice::all(); - } -} -``` - -You can now easily return the export class, without the need of calling `->download()` - -```php -return new InvoicesExport(); -``` \ No newline at end of file diff --git a/docs/export/extending.md b/docs/export/extending.md deleted file mode 100644 index c0703e654..000000000 --- a/docs/export/extending.md +++ /dev/null @@ -1,129 +0,0 @@ -# Extending - -### Events - -The export process has a few events you can leverage to interact with the underlying -classes to add custom behaviour to the export. - -You are able to hook into the parent package by using events. -No need to use convenience methods like "query" or "view", if you need full control over the export. - -The events will be activated by adding the `WithEvents` concern. Inside the `registerEvents` method, you -will have to return an array of events. The key is the FQN of the event and the value is a callable event listener. -This can either be a closure, array-callable or invokable class. - -```php -class InvoicesExport implements WithEvents -{ - /** - * @return array - */ - public function registerEvents(): array - { - return [ - // Handle by a closure. - BeforeExport::class => function(BeforeExport $event) { - $event->writer->getProperties()->setCreator('Patrick'); - }, - - // Array callable, refering to a static method. - BeforeWriting::class => [self::class, 'beforeWriting'], - - // Using a class with an __invoke method. - BeforeSheet::class => new BeforeSheetHandler() - ]; - } - - public static function beforeWriting(BeforeWriting $event) - { - // - } -} -``` - -Do note that using a `Closure` will not be possible in combination with queued exports, as PHP cannot serialize the closure. -In those cases it might be better to use the `RegistersEventListeners` trait. - -#### Auto register event listeners - -By using the `RegistersEventListeners` trait you can auto-register the event listeners, -without the need of using the `registerEvents`. The listener will only be registered if the method is created. - -```php -class InvoicesExport implements WithEvents -{ - use Exportable, RegistersEventListeners; - - public static function beforeExport(BeforeExport $event) - { - // - } - - public static function beforeWriting(BeforeWriting $event) - { - // - } - - public static function beforeSheet(BeforeSheet $event) - { - // - } - - public static function afterSheet(AfterSheet $event) - { - // - } -} -``` - -#### Available events - -| Event name | Payload | Explanation | -|---- |----| ----| -|`Maatwebsite\Excel\Events\BeforeExport` | `$event->writer : Writer` | Event gets raised at the start of the process. | -| `Maatwebsite\Excel\Events\BeforeWriting` | `$event->writer : Writer` | Event gets raised before the download/store starts. | -| `Maatwebsite\Excel\Events\BeforeSheet` | `$event->sheet : Sheet` | Event gets raised just after the sheet is created. | -| `Maatwebsite\Excel\Events\AfterSheet` | `$event->sheet : Sheet` | Event gets raised at the end of the sheet process. | - - -### Macroable - -Both `Writer` and `Sheet` are macroable which means they can easily be extended to fit your needs. - -#### Writer - -```php -Writer::macro('setCreator', function (Writer $writer, string $creator) { - $writer->getProperties()->setCreator($creator); -}); -``` - -#### Sheet - -```php -Sheet::macro('setOrientation', function (Sheet $sheet, $orientation) { - $sheet->getPageSetup()->setOrientation($orientation); -}); -``` - -Above examples could be used as: - -```php -class InvoicesExport implements WithEvents -{ - /** - * @return array - */ - public function registerEvents(): array - { - return [ - BeforeExport::class => function(BeforeExport $event) { - $event->writer->setCreator('Patrick'); - }, - AfterSheet::class => function(AfterSheet $event) { - $event->sheet->setOrientation(PageSetup::ORIENTATION_LANDSCAPE); - }, - ]; - } -} -``` \ No newline at end of file diff --git a/docs/export/from-query.md b/docs/export/from-query.md deleted file mode 100644 index 68eec43e1..000000000 --- a/docs/export/from-query.md +++ /dev/null @@ -1,90 +0,0 @@ -# From Query - -In the previous example, we did the query inside the export class. -While this is a good solution for small exports, -for bigger exports this will quickly become badly performant. - -By using the `FromQuery` concern, we can prepare a query for an export. Behind the scenes this query is executed in chunks. - -In the `InvoicesExport` class, add the `FromQuery` concern, and return a query. - -```php -namespace App\Exports; - -class InvoicesExport implements FromQuery -{ - use Exportable; - - public function query() - { - return Invoice::query(); - } -} -``` - -We can still download the export in the same way: - -```php -return (new InvoicesExport)->download('invoices.xlsx'); -``` - -### Customizing the query - -It's easy to pass on custom parameters to the query, -by simply passing them as dependencies to the export class. - -#### As constructor param - -```php -namespace App\Exports; - -class InvoicesExport implements FromQuery -{ - use Exportable; - - public function __construct(int $year) - { - $this->year = $year; - } - - public function query() - { - return Invoice::query()->whereYear('created_at', $this->year); - } -} -``` - -The year can now be passed as dependency to the export class: - -```php -return (new InvoicesExport(2018))->download('invoices.xlsx'); -``` - -#### As setter - -```php -namespace App\Exports; - -class InvoicesExport implements FromQuery -{ - use Exportable; - - public function forYear(int $year) - { - $this->year = $year; - - return $this; - } - - public function query() - { - return Invoice::query()->whereYear('created_at', $this->year); - } -} -``` - -We can adjust the year now by using the `forYear` method. - -```php -return (new InvoicesExport)->forYear(2018)->download('invoices.xlsx'); -``` \ No newline at end of file diff --git a/docs/export/from-view.md b/docs/export/from-view.md deleted file mode 100644 index 8532f0365..000000000 --- a/docs/export/from-view.md +++ /dev/null @@ -1,17 +0,0 @@ -# From View - -Exports can be created from a Blade view, by using the `FromView` concern. - -```php -use Illuminate\Contracts\View\View; - -class InvoicesExport implements FromView -{ - public function view(): View - { - return view('exports.invoices', [ - 'invoices' => Invoice::all() - ]); - } -} -``` \ No newline at end of file diff --git a/docs/export/mapping.md b/docs/export/mapping.md deleted file mode 100644 index 7961671b9..000000000 --- a/docs/export/mapping.md +++ /dev/null @@ -1,40 +0,0 @@ -# Mapping data - -### Mapping rows - -By adding `WithMapping` you map the data that needs to be added as row. -In case of using the Eloquent query builder: - -```php -class InvoicesExport implements FromQuery, WithMapping - - /** - * @var Invoice $invoice - */ - public function map($invoice): array - { - return [ - $invoice->invoice_number, - Date::dateTimeToExcel($invoice->created_at), - ]; - } -} -``` - -### Adding a heading row - -A heading row can easily be added by adding the `WithHeadings` concern. The heading row will be added -as very first row of the sheet. - -```php -class InvoicesExport implements FromQuery, WithHeadings - - public function headings(): array - { - return [ - '#', - 'Date', - ]; - } -} -``` \ No newline at end of file diff --git a/docs/export/multiple-sheets.md b/docs/export/multiple-sheets.md deleted file mode 100644 index a9b699dbb..000000000 --- a/docs/export/multiple-sheets.md +++ /dev/null @@ -1,76 +0,0 @@ -# Multiple Sheets - -To allow the export to have multiple sheets, the `WithMultipleSheets` concern should be used. -The `sheets()` method expect an array of sheet export objects to be returned. - -```php -class InvoicesExport implements WithMultipleSheets -{ - use Exportable; - - protected $year; - - public function __construct(int $year) - { - $this->year = $year; - } - - /** - * @return array - */ - public function sheets(): array - { - $sheets = []; - - for ($month = 1; $month <= 12; $month++) { - $sheets[] = new InvoicesPerMonthSheet($this->year, $month); - } - - return $sheets; - } -} -``` - -The `InvoicesPerMonthSheet` can implement concerns like `FromQuery`, `FromCollection`, ... - -```php -class InvoicesPerMonthSheet implements FromQuery, WithTitle -{ - private $month; - private $year; - - public function __construct(int $year, int $month) - { - $this->month = $month; - $this->year = $year; - } - - /** - * @return Builder - */ - public function query() - { - return Invoice - ::query() - ->whereYear('created_at', $this->year) - ->whereMonth('created_at', $this->month); - } - - /** - * @return string - */ - public function title(): string - { - return 'Month ' . $this->month; - } -} -``` - -This will now download an xlsx of all invoices in 2018, with 12 worksheets representing each month of the year. - -```php -public function download() -{ - return (new InvoicesExport(2018))->download('xlsx'); -} -``` \ No newline at end of file diff --git a/docs/export/queued.md b/docs/export/queued.md deleted file mode 100644 index 75887972e..000000000 --- a/docs/export/queued.md +++ /dev/null @@ -1,96 +0,0 @@ -# Queued - -In case you are working with a lot of data, it might be wise to queue the entire process. - -Given we have the following export class: - -```php -namespace App\Exports; - -class InvoicesExport implements FromQuery -{ - use Exportable; - - public function query() - { - return Invoice::query(); - } -} -``` - -It's as easy as calling `->queue()` now. - -```php -return (new InvoicesExport)->queue('invoices.xlsx'); -``` - -Behind the scenes the query will be chunked and multiple jobs will be chained. These jobs will be executed in the correct order, -and will only execute if none of the previous have failed. - -### Implicit Export queueing - -You can also mark an export implicitly as a queued export. You can do this by using Laravel's `ShouldQueue` contract. - -```php -namespace App\Exports; - -class InvoicesExport implements FromQuery, ShouldQueue -{ - use Exportable; - - public function query() - { - return Invoice::query(); - } -} -``` - -In your controller you can now call the normal `->store()` method. -Based on the presence of the `ShouldQueue` contract, the export will be queued. - -```php -return (new InvoicesExport)->store('invoices.xlsx'); -``` - -### Appending jobs - -The `queue()` method returns an instance of Laravel's `PendingDispatch`. This means you can chain extra jobs. - -```php -return (new InvoicesExport)->queue('invoices.xlsx')->chain([ - new InvoiceExportCompletedJob(), -]); -``` - -```php -class InvoiceExportCompletedJob implements ShouldQueue -{ - use Queueable; - - public function handle() - { - // Do something. - } -} -``` - -### Custom queues - -Because `PendingDispatch` is returned, we can also change the queue that should be used. - -```php -return (new InvoicesExport)->queue('invoices.xlsx')->allOnQueue('exports'); -``` - -### Chaining jobs - -It's also possible to chain extra jobs, that will be added to the end of the queue and only -executed if all export jobs are correctly executed. - -```php -return (new InvoicesExport) - ->queue('invoices.xlsx') - ->chain([ - new NotifyUserOfCompletedExport(request()->user()) - ]); -``` \ No newline at end of file diff --git a/docs/export/store.md b/docs/export/store.md deleted file mode 100644 index 7d9a7014d..000000000 --- a/docs/export/store.md +++ /dev/null @@ -1,17 +0,0 @@ -# Storing exports on disk - -Exports can easily be stored on any [filesystem](https://laravel.com/docs/5.6/filesystem) that Laravel supports. - -```php -public function storeExcel() -{ - // Store on default disk - Excel::store(new InvoicesExport(2018), 'invoices.xlsx'); - - // Store on a different disk (e.g. s3) - Excel::store(new InvoicesExport(2018), 'invoices.xlsx', 's3'); - - // Store on a different disk with a defined writer type. - Excel::store(new InvoicesExport(2018), 'invoices.xlsx', 's3', Excel::XLSX); -} -``` \ No newline at end of file diff --git a/docs/export/testing.md b/docs/export/testing.md deleted file mode 100644 index dd3c56894..000000000 --- a/docs/export/testing.md +++ /dev/null @@ -1,75 +0,0 @@ -# Testing - -The Excel facade can be used to swap the exporter to a fake. - -### Testing downloads - -```php -/** -* @test -*/ -public function user_can_download_invoices_export() -{ - Excel::fake(); - - $this->actingAs($this->givenUser()) - ->get('/invoices/download/xlsx'); - - Excel::assertDownloaded('filename.xlsx', function(InvoicesExport $export) { - // Assert that the correct export is downloaded. - return $export->collection()->contains('#2018-01'); - }); -} -``` - -### Testing storing exports - -```php -/** -* @test -*/ -public function user_can_store_invoices_export() -{ - Excel::fake(); - - $this->actingAs($this->givenUser()) - ->get('/invoices/store/xlsx'); - - Excel::assertStored('filename.xlsx', 'diskName'); - - Excel::assertStored('filename.xlsx', 'diskName', function(InvoicesExport $export) { - return true; - }); - - // When passing the callback as 2nd param, the disk will be the default disk. - Excel::assertStored('filename.xlsx', function(InvoicesExport $export) { - return true; - }); -} -``` - -### Testing queuing exports - -```php -/** -* @test -*/ -public function user_can_queue_invoices_export() -{ - Excel::fake(); - - $this->actingAs($this->givenUser()) - ->get('/invoices/queue/xlsx'); - - Excel::assertQueued('filename.xlsx', 'diskName'); - - Excel::assertQueued('filename.xlsx', 'diskName', function(InvoicesExport $export) { - return true; - }); - - // When passing the callback as 2nd param, the disk will be the default disk. - Excel::assertQueued('filename.xlsx', function(InvoicesExport $export) { - return true; - }); -} -``` \ No newline at end of file diff --git a/docs/getting-started.md b/docs/getting-started.md deleted file mode 100644 index 3cc31f389..000000000 --- a/docs/getting-started.md +++ /dev/null @@ -1,6 +0,0 @@ -@include:Introduction|basics -@include:License|license -@include:Installation|installation -@include:Upgrade Guide|upgrade -@include:Contributing|contributing -@include:Support|support diff --git a/docs/getting-started/basics.md b/docs/getting-started/basics.md deleted file mode 100644 index 40cd60b54..000000000 --- a/docs/getting-started/basics.md +++ /dev/null @@ -1,13 +0,0 @@ -# Introduction - -Laravel Excel is intended at being Laravel-flavoured PhpSpreadsheet: a simple, but elegant wrapper around PhpSpreadsheet with the goal of simplifying -exports. - -[PhpSpreadsheet](https://phpspreadsheet.readthedocs.io/) is a library written in pure PHP and providing a set of classes that allow you to read from and to write to different spreadsheet file formats, like Excel and LibreOffice Calc. - -Laravel Excel features: - -* Easily export collections to Excel -* Export queries with automatic chunking for better performance -* Queue exports for better performance -* Easily export Blade views to Excel \ No newline at end of file diff --git a/docs/getting-started/contributing.md b/docs/getting-started/contributing.md deleted file mode 100644 index d9da7d936..000000000 --- a/docs/getting-started/contributing.md +++ /dev/null @@ -1,47 +0,0 @@ -# Contributing - -Please read and understand the contribution guide before creating an issue or pull request. - -## Links - -- [Docs](https://laravel-excel.maatwebsite.nl) -- [Issue tracker](https://github.com/Maatwebsite/Laravel-Excel/issues) -- [Support](https://laravel-excel.maatwebsite.nl/docs/3.0/getting-started/support) - -## Etiquette - -This project is an open source project, and as such, the maintainers use their free time to build and maintain it. -The code is freely available and can be used, forked and modified. - -Please be considerate towards maintainers when raising issues or presenting pull requests. - -It's the duty of the maintainer to ensure that all submissions to the project are of sufficient -quality to benefit the project. Many developers have different skill sets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. - -## Viability - -When requesting or submitting new features, first consider whether it might be useful to others. Open -source projects are used by many developers, who may have entirely different needs to your own. Think about -whether or not your feature is likely to be used by other users of the project. - -## How to submit changes? - -- Check the codebase to ensure that your feature doesn't already exist. -- Check the pull requests to ensure that another person hasn't already submitted the feature or fix. -- Use the [pull request template](https://github.com/Maatwebsite/Laravel-Excel/blob/3.0/.github/PULL_REQUEST_TEMPLATE.md) - -## How to report a bug? - -- Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. -- Check to make sure your feature suggestion isn't already present within the project. -- Check the pull requests tab to ensure that the bug doesn't have a fix in progress. -- Check the pull requests tab to ensure that the feature isn't already in progress. -- Use the [issue template](https://github.com/Maatwebsite/Laravel-Excel/blob/3.0/.github/ISSUE_TEMPLATE.md). - -## Requirements - -- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)**. - Style will get automatically fixed by StyleCI. -- **Add tests backing up your change!** - You can run the test by running `vendor/bin/phpunit` -- **Document any change in behaviour** - Documentation is located in the `/docs/` folder -- **One feature per Pull Request** -- **Add meaningful commit messages** diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md deleted file mode 100644 index 611e50127..000000000 --- a/docs/getting-started/installation.md +++ /dev/null @@ -1,119 +0,0 @@ -# Installation - -### Requirements - -* PHP: ^7.1 -* Laravel: ^5.5 -* PhpSpreadsheet: ^1.2 -* PHP extension php_zip enabled -* PHP extension php_xml enabled -* PHP extension php_gd2 enabled - -##### PHP version support -Support for PHP versions will only be maintained for a period of six months beyond the end-of-life of that PHP version - -### Supported Versions - -Versions will be supported for a limited amount of time. - -| Version | Laravel Version | Php Version | Support | -|---- |----|----|----| -| 2.1 | <=5.6 | <=7.0 | EOL on 15-05-2018 | -| 3.0 | ^5.5 | ^7.0 | New features | - -### Composer - -Require this package in the `composer.json` of your Laravel project. This will download the package and PhpSpreadsheet. - -``` -composer require maatwebsite/excel -``` - -### Service Provider - -The `Maatwebsite\Excel\ExcelServiceProvider` is auto-discovered and registered by default, but if you want to register it yourself: - -Add the ServiceProvider in `app/config/app.php` - -```php -'providers' => [ - /* - * Package Service Providers... - */ - Maatwebsite\Excel\ExcelServiceProvider::class, -] -``` - -### Facade - -The `Excel` facade is also auto-discovered, but if you want to add it manually: - -Add the Facade in `app/config/app.php` - -```php -'aliases' => [ - ... - 'Excel' => Maatwebsite\Excel\Facades\Excel::class, -] -``` - -### Config - -To publish the config, run the vendor publish command: - -``` -php artisan vendor:publish -``` - -This will create a new config file named `config/excel.php`. - -### Usage - -You can use Excel in the following ways: - -Via dependency injection: - -```php -public function __construct(\Maatwebsite\Excel\Excel $excel) -{ - $this->excel = $excel; -} - -public function export() -{ - return $this->excel->export(new Export); -} - -``` - -Via `Exporter` interface: - -```php -public function __construct(\Maatwebsite\Excel\Exporter $excel) -{ - $this->excel = $excel; -} - -public function export() -{ - return $this->excel->export(new Export); -} - -``` - -Via the Facade - -```php -public function export() -{ - return Excel::export(new Export); -} -``` - -Via container binding: - -```php -$this->app->bind(Exporter::class, function() { - return new Exporter($this->app['excel']); -}); -``` diff --git a/docs/getting-started/license.md b/docs/getting-started/license.md deleted file mode 100644 index 6e32e266a..000000000 --- a/docs/getting-started/license.md +++ /dev/null @@ -1,14 +0,0 @@ -# License - -Our software is open source and licensed under the [MIT license](https://choosealicense.com/licenses/mit/). According to the [postcardware](https://en.wikipedia.org/wiki/Postcardware) concept, if you use the software in your production environment we would appreciate to receive a postcard of your hometown. Please send it to: - -**Maatwebsite** -Florijnruwe 111-2 -6218 CA Maastricht -The Netherlands - -You are free to use the software as you like. The code can be forked and modified, but the original copyright author should always be included! - -We hold no liability and will provide support on a best effort basis. For more information about support please see [support](https://laravel-excel.maatwebsite.nl/docs/3.0/getting-started/support). - -If you use the software commercially and need support urgently, we can offer this on a commercial basis. Please contact or via phone +31 (0)10 744 9312. diff --git a/docs/getting-started/support.md b/docs/getting-started/support.md deleted file mode 100644 index 0b6cabf7c..000000000 --- a/docs/getting-started/support.md +++ /dev/null @@ -1,26 +0,0 @@ -# Support - -Our software is free and open source, meaning that the use of our software is optional. We hold no liability and there is no obligation to support. We will provide support on a best effort basis. - -If you use the software commercially and need elaborate support or need it urgently, we can offer this on a commercial basis. Please contact or via phone +31 (0)10 744 9312. - -### Supported Versions -Versions will be supported for a limited amount of time. - -| Version | Laravel Version | Php Version | Support | -|---- |----|----|----| -| 2.1 | <=5.6 | <=7.0 | EOL on 15-05-2018 | -| 3.0 | ^5.5 | ^7.0 | New features | - -### Requesting support -Before you request support, please check the following: -* Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. -* Check to make sure your support request isn't already present within the project. -* Check the pull requests tab to ensure that the bug doesn't have a fix in progress. -* Use the [issue template](https://github.com/Maatwebsite/Laravel-Excel/blob/3.0/.github/ISSUE_TEMPLATE.md). - -Filling out the template is required. Issues that do not include enough information might not be picked up. - -Please prefix your issue with one of the following: [BUG] [PROPOSAL] [QUESTION]. - -And please be considerate towards maintainers when raising issues. diff --git a/docs/getting-started/upgrade.md b/docs/getting-started/upgrade.md deleted file mode 100644 index 9ec96b391..000000000 --- a/docs/getting-started/upgrade.md +++ /dev/null @@ -1,11 +0,0 @@ -# Upgrade Guide - -### Upgrading to 3.0 from 2.* - -Version 3.0 will not be backwards compatible with 2.*. It's not possible to provide a migration guide. - -#### New dependencies - -* Requires PHP 7.0 or higher. -* Requires Laravel 5.5 (or higher). -* Requires PhpSpreadsheet instead of PHPExcel. diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 3de1e7b25..000000000 --- a/docs/index.md +++ /dev/null @@ -1,2 +0,0 @@ -@include:Getting started|getting-started -@include:Exports|export \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist index df48be590..194d65a97 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -8,7 +8,6 @@ convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" - syntaxCheck="true" > diff --git a/src/Concerns/WithCharts.php b/src/Concerns/WithCharts.php new file mode 100644 index 000000000..b40be0ffa --- /dev/null +++ b/src/Concerns/WithCharts.php @@ -0,0 +1,13 @@ +count() by the chunk size. + * Depending on the implementation of the query() method (eg. When using a groupBy clause), this calculation might not be correct. + * + * When this is the case, you should use this method to provide a custom calculation of the query size. + * + * @return int + */ + public function querySize(): int; +} diff --git a/src/Concerns/WithCustomStartCell.php b/src/Concerns/WithCustomStartCell.php new file mode 100644 index 000000000..bff03f850 --- /dev/null +++ b/src/Concerns/WithCustomStartCell.php @@ -0,0 +1,11 @@ +option('model') && $this->option('query')) { + $stub = '/stubs/export.query-model.stub'; + } elseif ($this->option('model')) { + $stub = '/stubs/export.model.stub'; + } elseif ($this->option('query')) { + $stub = '/stubs/export.query.stub'; + } + + $stub = $stub ?? '/stubs/export.plain.stub'; + + return __DIR__ . $stub; + } + + /** + * Get the default namespace for the class. + * + * @param string $rootNamespace + * + * @return string + */ + protected function getDefaultNamespace($rootNamespace) + { + return $rootNamespace . '\Exports'; + } + + /** + * Build the class with the given name. + * Remove the base controller import if we are already in base namespace. + * + * @param string $name + * + * @return string + */ + protected function buildClass($name) + { + $replace = []; + if ($this->option('model')) { + $replace = $this->buildModelReplacements($replace); + } + + return str_replace( + array_keys($replace), array_values($replace), parent::buildClass($name) + ); + } + + /** + * Build the model replacement values. + * + * @param array $replace + * + * @return array + */ + protected function buildModelReplacements(array $replace): array + { + $modelClass = $this->parseModel($this->option('model')); + + return array_merge($replace, [ + 'DummyFullModelClass' => $modelClass, + 'DummyModelClass' => class_basename($modelClass), + ]); + } + + /** + * Get the fully-qualified model class name. + * + * @param string $model + * + * @return string + */ + protected function parseModel($model): string + { + if (preg_match('([^A-Za-z0-9_/\\\\])', $model)) { + throw new InvalidArgumentException('Model name contains invalid characters.'); + } + + $model = trim(str_replace('/', '\\', $model), '\\'); + + if (!Str::startsWith($model, $rootNamespace = $this->laravel->getNamespace())) { + $model = $rootNamespace . $model; + } + + return $model; + } + + /** + * Get the console command options. + * + * @return array + */ + protected function getOptions() + { + return [ + ['model', 'm', InputOption::VALUE_OPTIONAL, 'Generate an export for the given model.'], + ['query', '', InputOption::VALUE_NONE, 'Generate an export for a query.'], + ]; + } +} diff --git a/src/Console/stubs/export.model.stub b/src/Console/stubs/export.model.stub new file mode 100644 index 000000000..af6f6b839 --- /dev/null +++ b/src/Console/stubs/export.model.stub @@ -0,0 +1,17 @@ +sheet = $sheet; + $this->sheet = $sheet; + $this->exportable = $exportable; } /** @@ -26,4 +33,20 @@ public function getSheet(): Sheet { return $this->sheet; } + + /** + * @return object + */ + public function getConcernable() + { + return $this->exportable; + } + + /** + * @return mixed + */ + public function getDelegate() + { + return $this->sheet; + } } diff --git a/src/Events/BeforeExport.php b/src/Events/BeforeExport.php index 8f289bdc6..dee003ae4 100644 --- a/src/Events/BeforeExport.php +++ b/src/Events/BeforeExport.php @@ -4,19 +4,26 @@ use Maatwebsite\Excel\Writer; -class BeforeExport +class BeforeExport extends Event { /** * @var Writer */ public $writer; + /** + * @var object + */ + private $exportable; + /** * @param Writer $writer + * @param object $exportable */ - public function __construct(Writer $writer) + public function __construct(Writer $writer, $exportable) { - $this->writer = $writer; + $this->writer = $writer; + $this->exportable = $exportable; } /** @@ -26,4 +33,20 @@ public function getWriter(): Writer { return $this->writer; } + + /** + * @return object + */ + public function getConcernable() + { + return $this->exportable; + } + + /** + * @return mixed + */ + public function getDelegate() + { + return $this->writer; + } } diff --git a/src/Events/BeforeSheet.php b/src/Events/BeforeSheet.php index 844bf5ec0..170c95b15 100644 --- a/src/Events/BeforeSheet.php +++ b/src/Events/BeforeSheet.php @@ -4,7 +4,7 @@ use Maatwebsite\Excel\Sheet; -class BeforeSheet +class BeforeSheet extends Event { /** * @var Sheet @@ -12,11 +12,18 @@ class BeforeSheet public $sheet; /** - * @param Sheet $sheet + * @var object */ - public function __construct(Sheet $sheet) + private $exportable; + + /** + * @param Sheet $sheet + * @param object $exportable + */ + public function __construct(Sheet $sheet, $exportable) { - $this->sheet = $sheet; + $this->sheet = $sheet; + $this->exportable = $exportable; } /** @@ -26,4 +33,20 @@ public function getSheet(): Sheet { return $this->sheet; } + + /** + * @return object + */ + public function getConcernable() + { + return $this->exportable; + } + + /** + * @return mixed + */ + public function getDelegate() + { + return $this->sheet; + } } diff --git a/src/Events/BeforeWriting.php b/src/Events/BeforeWriting.php index 11e1ab940..137feec7c 100644 --- a/src/Events/BeforeWriting.php +++ b/src/Events/BeforeWriting.php @@ -4,19 +4,26 @@ use Maatwebsite\Excel\Writer; -class BeforeWriting +class BeforeWriting extends Event { /** * @var Writer */ public $writer; + /** + * @var object + */ + private $exportable; + /** * @param Writer $writer + * @param object $exportable */ - public function __construct(Writer $writer) + public function __construct(Writer $writer, $exportable) { - $this->writer = $writer; + $this->writer = $writer; + $this->exportable = $exportable; } /** @@ -26,4 +33,20 @@ public function getWriter(): Writer { return $this->writer; } + + /** + * @return object + */ + public function getConcernable() + { + return $this->exportable; + } + + /** + * @return mixed + */ + public function getDelegate() + { + return $this->writer; + } } diff --git a/src/Events/Event.php b/src/Events/Event.php new file mode 100644 index 000000000..2e2f587e3 --- /dev/null +++ b/src/Events/Event.php @@ -0,0 +1,26 @@ +getConcernable() instanceof $concern; + } +} diff --git a/src/Excel.php b/src/Excel.php index b2ccdcf46..833da7d52 100644 --- a/src/Excel.php +++ b/src/Excel.php @@ -8,10 +8,14 @@ class Excel implements Exporter { + use RegistersCustomConcerns; + const XLSX = 'Xlsx'; const CSV = 'Csv'; + const TSV = 'Csv'; + const ODS = 'Ods'; const XLS = 'Xls'; diff --git a/src/ExcelServiceProvider.php b/src/ExcelServiceProvider.php index 7854a98bd..52056a51c 100644 --- a/src/ExcelServiceProvider.php +++ b/src/ExcelServiceProvider.php @@ -5,8 +5,10 @@ use Illuminate\Support\Collection; use Illuminate\Support\ServiceProvider; use Maatwebsite\Excel\Mixins\StoreCollection; +use Maatwebsite\Excel\Console\ExportMakeCommand; use Maatwebsite\Excel\Mixins\DownloadCollection; use Illuminate\Contracts\Routing\ResponseFactory; +use Laravel\Lumen\Application as LumenApplication; class ExcelServiceProvider extends ServiceProvider { @@ -16,9 +18,13 @@ class ExcelServiceProvider extends ServiceProvider public function boot() { if ($this->app->runningInConsole()) { - $this->publishes([ - $this->getConfigFile() => config_path('excel.php'), - ], 'config'); + if ($this->app instanceof LumenApplication) { + $this->app->configure('excel'); + } else { + $this->publishes([ + $this->getConfigFile() => config_path('excel.php'), + ], 'config'); + } } } @@ -46,6 +52,10 @@ public function register() Collection::mixin(new DownloadCollection); Collection::mixin(new StoreCollection); + + $this->commands([ + ExportMakeCommand::class, + ]); } /** diff --git a/src/HasEventBus.php b/src/HasEventBus.php index fc796fb7a..9b4c329f6 100644 --- a/src/HasEventBus.php +++ b/src/HasEventBus.php @@ -4,28 +4,37 @@ trait HasEventBus { + /** + * @var array + */ + protected static $globalEvents = []; + /** * @var array */ protected $events = []; /** + * Register local event listeners. + * * @param array $listeners */ public function registerListeners(array $listeners) { foreach ($listeners as $event => $listener) { - $this->listen($event, $listener); + $this->events[$event][] = $listener; } } /** + * Register a global event listener. + * * @param string $event * @param callable $listener */ - public function listen(string $event, callable $listener) + public static function listen(string $event, callable $listener) { - $this->events[$event][] = $listener; + static::$globalEvents[$event][] = $listener; } /** @@ -33,10 +42,23 @@ public function listen(string $event, callable $listener) */ public function raise($event) { - $listeners = $this->events[get_class($event)] ?? []; - - foreach ($listeners as $listener) { + foreach ($this->listeners($event) as $listener) { $listener($event); } } + + /** + * @param object $event + * + * @return callable[] + */ + public function listeners($event): array + { + $name = \get_class($event); + + $localListeners = $this->events[$name] ?? []; + $globalListeners = static::$globalEvents[$name] ?? []; + + return array_merge($globalListeners, $localListeners); + } } diff --git a/src/Jobs/AppendDataToSheet.php b/src/Jobs/AppendDataToSheet.php index db7778aa4..1e8ff9f8a 100644 --- a/src/Jobs/AppendDataToSheet.php +++ b/src/Jobs/AppendDataToSheet.php @@ -67,6 +67,6 @@ public function handle(Writer $writer) $sheet->appendRows($this->data, $this->sheetExport); - $writer->write($this->filePath, $this->writerType); + $writer->write($this->sheetExport, $this->filePath, $this->writerType); } } diff --git a/src/Jobs/AppendQueryToSheet.php b/src/Jobs/AppendQueryToSheet.php index 51fb7e3b8..6eade7dbb 100644 --- a/src/Jobs/AppendQueryToSheet.php +++ b/src/Jobs/AppendQueryToSheet.php @@ -72,6 +72,6 @@ public function handle(Writer $writer) $sheet->appendRows($this->query->execute(), $this->sheetExport); - $writer->write($this->filePath, $this->writerType); + $writer->write($this->sheetExport, $this->filePath, $this->writerType); } } diff --git a/src/Jobs/CloseSheet.php b/src/Jobs/CloseSheet.php index 788b15c8a..d7aaf3687 100644 --- a/src/Jobs/CloseSheet.php +++ b/src/Jobs/CloseSheet.php @@ -64,6 +64,6 @@ public function handle(Writer $writer) $sheet->close($this->sheetExport); - $writer->write($this->filePath, $this->writerType); + $writer->write($this->sheetExport, $this->filePath, $this->writerType); } } diff --git a/src/Jobs/QueueExport.php b/src/Jobs/QueueExport.php index 4bb8e5c18..10acb7d1f 100644 --- a/src/Jobs/QueueExport.php +++ b/src/Jobs/QueueExport.php @@ -60,6 +60,6 @@ public function handle(Writer $writer) } // Write to temp file with empty sheets. - $writer->write($this->tempFile, $this->writerType); + $writer->write($sheetExport, $this->tempFile, $this->writerType); } } diff --git a/src/Jobs/SerializedQuery.php b/src/Jobs/SerializedQuery.php index 27a3f5832..09a5c12e3 100644 --- a/src/Jobs/SerializedQuery.php +++ b/src/Jobs/SerializedQuery.php @@ -5,7 +5,9 @@ use Illuminate\Database\Connection; use Illuminate\Support\Facades\Event; use Illuminate\Database\Query\Builder; +use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Events\StatementPrepared; +use Illuminate\Database\Eloquent\Builder as EloquentBuilder; class SerializedQuery { @@ -24,6 +26,11 @@ class SerializedQuery */ public $connection; + /** + * @var string|null + */ + public $model; + /** * @param Builder $builder */ @@ -32,6 +39,10 @@ public function __construct($builder) $this->query = $builder->toSql(); $this->bindings = $builder->getBindings(); $this->connection = $builder->getConnection()->getName(); + + if ($builder instanceof EloquentBuilder) { + $this->model = get_class($builder->getModel()); + } } /** @@ -46,6 +57,41 @@ public function execute() $event->statement->setFetchMode(\PDO::FETCH_ASSOC); }); - return $connection->select($this->query, $this->bindings); + return $this->hydrate( + $connection->select($this->query, $this->bindings) + ); + } + + /** + * @param array $items + * + * @return mixed + */ + public function hydrate(array $items) + { + if (!$instance = $this->newModelInstance()) { + return $items; + } + + return array_map(function ($item) use ($instance) { + return $instance->newFromBuilder($item); + }, $items); + } + + /** + * @return Model|null + */ + private function newModelInstance() + { + if (null === $this->model) { + return null; + } + + /** @var Model $model */ + $model = new $this->model; + + $model->setConnection($this->connection); + + return $model; } } diff --git a/src/Mixins/DownloadCollection.php b/src/Mixins/DownloadCollection.php index 0b134ff6a..f695ba959 100644 --- a/src/Mixins/DownloadCollection.php +++ b/src/Mixins/DownloadCollection.php @@ -2,8 +2,11 @@ namespace Maatwebsite\Excel\Mixins; +use Maatwebsite\Excel\Sheet; use Illuminate\Support\Collection; use Maatwebsite\Excel\Concerns\Exportable; +use Illuminate\Contracts\Support\Arrayable; +use Maatwebsite\Excel\Concerns\WithHeadings; use Maatwebsite\Excel\Concerns\FromCollection; class DownloadCollection @@ -13,10 +16,15 @@ class DownloadCollection */ public function downloadExcel() { - return function (string $fileName, string $writerType = null) { - $export = new class($this) implements FromCollection { + return function (string $fileName, string $writerType = null, $withHeadings = false) { + $export = new class($this, $withHeadings) implements FromCollection, WithHeadings { use Exportable; + /** + * @var bool + */ + private $withHeadings; + /** * @var Collection */ @@ -24,10 +32,12 @@ public function downloadExcel() /** * @param Collection $collection + * @param bool $withHeading */ - public function __construct(Collection $collection) + public function __construct(Collection $collection, bool $withHeading = false) { - $this->collection = $collection->toBase(); + $this->collection = $collection->toBase(); + $this->withHeadings = $withHeading; } /** @@ -37,6 +47,24 @@ public function collection() { return $this->collection; } + + /** + * @return array + */ + public function headings(): array + { + if (!$this->withHeadings) { + return []; + } + + $firstRow = $this->collection->first(); + + if ($firstRow instanceof Arrayable || \is_object($firstRow)) { + return array_keys(Sheet::mapArraybleRow($firstRow)); + } + + return $this->collection->collapse()->keys()->all(); + } }; return $export->download($fileName, $writerType); diff --git a/src/Mixins/StoreCollection.php b/src/Mixins/StoreCollection.php index fcf22565e..5dcd57fa2 100644 --- a/src/Mixins/StoreCollection.php +++ b/src/Mixins/StoreCollection.php @@ -4,6 +4,7 @@ use Illuminate\Support\Collection; use Maatwebsite\Excel\Concerns\Exportable; +use Maatwebsite\Excel\Concerns\WithHeadings; use Maatwebsite\Excel\Concerns\FromCollection; class StoreCollection @@ -13,10 +14,15 @@ class StoreCollection */ public function storeExcel() { - return function (string $filePath, string $disk = null, string $writerType = null) { - $export = new class($this) implements FromCollection { + return function (string $filePath, string $disk = null, string $writerType = null, $withHeadings = false) { + $export = new class($this, $withHeadings) implements FromCollection, WithHeadings { use Exportable; + /** + * @var bool + */ + private $withHeadings; + /** * @var Collection */ @@ -24,10 +30,12 @@ public function storeExcel() /** * @param Collection $collection + * @param bool $withHeadings */ - public function __construct(Collection $collection) + public function __construct(Collection $collection, bool $withHeadings = false) { - $this->collection = $collection->toBase(); + $this->collection = $collection->toBase(); + $this->withHeadings = $withHeadings; } /** @@ -37,6 +45,14 @@ public function collection() { return $this->collection; } + + /** + * @return array + */ + public function headings(): array + { + return $this->withHeadings ? $this->collection->collapse()->keys()->all() : []; + } }; return $export->store($filePath, $disk, $writerType); diff --git a/src/QueuedWriter.php b/src/QueuedWriter.php index 9abf97c28..f152aff4f 100644 --- a/src/QueuedWriter.php +++ b/src/QueuedWriter.php @@ -2,17 +2,19 @@ namespace Maatwebsite\Excel; +use Traversable; use Illuminate\Support\Collection; use Maatwebsite\Excel\Jobs\CloseSheet; use Maatwebsite\Excel\Jobs\QueueExport; use Maatwebsite\Excel\Concerns\FromQuery; -use Illuminate\Contracts\Support\Arrayable; use Maatwebsite\Excel\Jobs\SerializedQuery; use Maatwebsite\Excel\Jobs\AppendDataToSheet; use Maatwebsite\Excel\Jobs\StoreQueuedExport; use Maatwebsite\Excel\Concerns\FromCollection; use Maatwebsite\Excel\Jobs\AppendQueryToSheet; use Maatwebsite\Excel\Concerns\WithMultipleSheets; +use Maatwebsite\Excel\Concerns\WithCustomChunkSize; +use Maatwebsite\Excel\Concerns\WithCustomQuerySize; class QueuedWriter { @@ -98,10 +100,10 @@ private function exportCollection( ) { return $export ->collection() - ->chunk($this->chunkSize) + ->chunk($this->getChunkSize($export)) ->map(function ($rows) use ($writerType, $filePath, $sheetIndex, $export) { - if ($rows instanceof Arrayable) { - $rows = $rows->toArray(); + if ($rows instanceof Traversable) { + $rows = iterator_to_array($rows); } return new AppendDataToSheet( @@ -130,12 +132,14 @@ private function exportQuery( ) { $query = $export->query(); - $jobs = new Collection(); - $spins = ceil($query->count() / $this->chunkSize); + $count = $export instanceof WithCustomQuerySize ? $export->querySize() : $query->count(); + $spins = ceil($count / $this->getChunkSize($export)); + + $jobs = new Collection(); for ($page = 1; $page <= $spins; $page++) { $serializedQuery = new SerializedQuery( - $query->forPage($page, $this->chunkSize) + $query->forPage($page, $this->getChunkSize($export)) ); $jobs->push(new AppendQueryToSheet( @@ -149,4 +153,18 @@ private function exportQuery( return $jobs; } + + /** + * @param object|WithCustomChunkSize $export + * + * @return int + */ + private function getChunkSize($export): int + { + if ($export instanceof WithCustomChunkSize) { + return $export->chunkSize(); + } + + return $this->chunkSize; + } } diff --git a/src/RegistersCustomConcerns.php b/src/RegistersCustomConcerns.php new file mode 100644 index 000000000..355e5f7aa --- /dev/null +++ b/src/RegistersCustomConcerns.php @@ -0,0 +1,39 @@ + Writer::class, + BeforeExport::class => Writer::class, + BeforeSheet::class => Sheet::class, + AfterSheet::class => Sheet::class, + ]; + + /** + * @param string $concern + * @param callable $handler + * @param string $event + */ + public static function extend(string $concern, callable $handler, string $event = BeforeWriting::class) + { + /** @var HasEventBus $delegate */ + $delegate = static::$eventMap[$event] ?? BeforeWriting::class; + + $delegate::listen($event, function (Event $event) use ($concern, $handler) { + if ($event->appliesToConcern($concern)) { + $handler($event->getConcernable(), $event->getDelegate()); + } + }); + } +} diff --git a/src/Sheet.php b/src/Sheet.php index d1d8480ad..d3beac73d 100644 --- a/src/Sheet.php +++ b/src/Sheet.php @@ -8,15 +8,22 @@ use Maatwebsite\Excel\Concerns\FromQuery; use Maatwebsite\Excel\Concerns\WithTitle; use Maatwebsite\Excel\Events\BeforeSheet; +use PhpOffice\PhpSpreadsheet\Chart\Chart; use PhpOffice\PhpSpreadsheet\Reader\Html; +use Maatwebsite\Excel\Concerns\WithCharts; use Maatwebsite\Excel\Concerns\WithEvents; use Illuminate\Contracts\Support\Arrayable; use Maatwebsite\Excel\Concerns\WithMapping; +use Maatwebsite\Excel\Concerns\WithDrawings; use Maatwebsite\Excel\Concerns\WithHeadings; use Maatwebsite\Excel\Concerns\FromCollection; use Maatwebsite\Excel\Concerns\ShouldAutoSize; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; +use Maatwebsite\Excel\Concerns\WithCustomChunkSize; +use Maatwebsite\Excel\Concerns\WithCustomStartCell; +use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing; use Maatwebsite\Excel\Concerns\WithColumnFormatting; +use Maatwebsite\Excel\Concerns\WithStrictNullComparison; use Maatwebsite\Excel\Exceptions\ConcernConflictException; class Sheet @@ -33,6 +40,11 @@ class Sheet */ protected $tmpPath; + /** + * @var object + */ + protected $exportable; + /** * @var Worksheet */ @@ -55,11 +67,13 @@ public function __construct(Worksheet $worksheet) */ public function open($sheetExport) { + $this->exportable = $sheetExport; + if ($sheetExport instanceof WithEvents) { $this->registerListeners($sheetExport->registerEvents()); } - $this->raise(new BeforeSheet($this)); + $this->raise(new BeforeSheet($this, $this->exportable)); if ($sheetExport instanceof WithTitle) { $this->worksheet->setTitle($sheetExport->title()); @@ -70,7 +84,19 @@ public function open($sheetExport) } if (!$sheetExport instanceof FromView && $sheetExport instanceof WithHeadings) { - $this->append([$sheetExport->headings()]); + if ($sheetExport instanceof WithCustomStartCell) { + $startCell = $sheetExport->startCell(); + } + + $this->append([$sheetExport->headings()], $startCell ?? null, $this->hasStrictNullComparison($sheetExport)); + } + + if ($sheetExport instanceof WithCharts) { + $this->addCharts($sheetExport->charts()); + } + + if ($sheetExport instanceof WithDrawings) { + $this->addDrawings($sheetExport->drawings()); } } @@ -92,7 +118,7 @@ public function export($sheetExport) } if ($sheetExport instanceof FromCollection) { - $this->fromCollection($sheetExport, $this->worksheet); + $this->fromCollection($sheetExport); } } @@ -106,6 +132,8 @@ public function export($sheetExport) */ public function close($sheetExport) { + $this->exportable = $sheetExport; + if ($sheetExport instanceof WithColumnFormatting) { foreach ($sheetExport->columnFormats() as $column => $format) { $this->formatColumn($column, $format); @@ -116,7 +144,7 @@ public function close($sheetExport) $this->autoSize(); } - $this->raise(new AfterSheet($this)); + $this->raise(new AfterSheet($this, $this->exportable)); } /** @@ -133,7 +161,9 @@ public function fromView(FromView $sheetExport) /** @var Html $reader */ $reader = IOFactory::createReader('Html'); - $reader->setSheetIndex($spreadsheet->getActiveSheetIndex()); + + // Insert content into the last sheet + $reader->setSheetIndex($spreadsheet->getSheetCount() - 1); $reader->loadIntoExisting($tempFile, $spreadsheet); } @@ -143,7 +173,7 @@ public function fromView(FromView $sheetExport) */ public function fromQuery(FromQuery $sheetExport, Worksheet $worksheet) { - $sheetExport->query()->chunk($this->chunkSize, function ($chunk) use ($sheetExport, $worksheet) { + $sheetExport->query()->chunk($this->getChunkSize($sheetExport), function ($chunk) use ($sheetExport, $worksheet) { foreach ($chunk as $row) { $this->appendRow($row, $sheetExport); } @@ -152,33 +182,30 @@ public function fromQuery(FromQuery $sheetExport, Worksheet $worksheet) /** * @param FromCollection $sheetExport - * @param Worksheet $worksheet */ - public function fromCollection(FromCollection $sheetExport, Worksheet $worksheet) + public function fromCollection(FromCollection $sheetExport) { - $sheetExport - ->collection() - ->each(function ($row) use ($sheetExport, $worksheet) { - $this->appendRow($row, $sheetExport); - }); + $this->appendRows($sheetExport->collection()->all(), $sheetExport); } /** - * @param array $rows - * @param int|null $row + * @param array $rows + * @param string|null $startCell + * @param bool $strictNullComparison * * @throws \PhpOffice\PhpSpreadsheet\Exception */ - public function append(array $rows, int $row = null) + public function append(array $rows, string $startCell = null, bool $strictNullComparison = false) { - if (!$row) { - $row = 1; - if ($this->hasRows()) { - $row = $this->worksheet->getHighestRow() + 1; - } + if (!$startCell) { + $startCell = 'A1'; + } + + if ($this->hasRows()) { + $startCell = 'A' . ($this->worksheet->getHighestRow() + 1); } - $this->worksheet->fromArray($rows, null, 'A' . $row); + $this->worksheet->fromArray($rows, null, $startCell, $strictNullComparison); } /** @@ -186,7 +213,7 @@ public function append(array $rows, int $row = null) */ public function autoSize() { - foreach (range('A', $this->worksheet->getHighestDataColumn()) as $col) { + foreach ($this->buildColumnRange('A', $this->worksheet->getHighestDataColumn()) as $col) { $this->worksheet->getColumnDimension($col)->setAutoSize(true); } } @@ -225,6 +252,40 @@ public function getDelegate() return $this->worksheet; } + /** + * @param Chart|Chart[] $charts + */ + public function addCharts($charts) + { + $charts = \is_array($charts) ? $charts : [$charts]; + + foreach ($charts as $chart) { + $this->worksheet->addChart($chart); + } + } + + /** + * @param BaseDrawing|BaseDrawing[] $drawings + */ + public function addDrawings($drawings) + { + $drawings = \is_array($drawings) ? $drawings : [$drawings]; + + foreach ($drawings as $drawing) { + $drawing->setWorksheet($this->worksheet); + } + } + + /** + * @param string $concern + * + * @return string + */ + public function hasConcern(string $concern): string + { + return $this->exportable instanceof $concern; + } + /** * @param iterable $rows * @param object $sheetExport @@ -239,10 +300,39 @@ public function appendRows($rows, $sheetExport) $row = $sheetExport->map($row); } - $append[] = $row; + $append[] = static::mapArraybleRow($row); } - $this->append($append); + if ($sheetExport instanceof WithCustomStartCell) { + $startCell = $sheetExport->startCell(); + } + + $this->append($append, $startCell ?? null, $this->hasStrictNullComparison($sheetExport)); + } + + /** + * @param mixed $row + * + * @return array + */ + public static function mapArraybleRow($row): array + { + // When dealing with eloquent models, we'll skip the relations + // as we won't be able to display them anyway. + if (method_exists($row, 'attributesToArray')) { + return $row->attributesToArray(); + } + + if ($row instanceof Arrayable) { + return $row->toArray(); + } + + // Convert StdObjects to arrays + if (is_object($row)) { + return json_decode(json_encode($row), true); + } + + return $row; } /** @@ -257,11 +347,31 @@ protected function appendRow($row, $sheetExport) $row = $sheetExport->map($row); } - if ($row instanceof Arrayable) { - $row = $row->toArray(); + $row = static::mapArraybleRow($row); + + if ($sheetExport instanceof WithCustomStartCell) { + $startCell = $sheetExport->startCell(); } - $this->append([$row]); + if (isset($row[0]) && is_array($row[0])) { + $this->append($row, $startCell ?? null, $this->hasStrictNullComparison($sheetExport)); + } else { + $this->append([$row], $startCell ?? null, $this->hasStrictNullComparison($sheetExport)); + } + } + + /** + * @param string $lower + * @param string $upper + * + * @return \Generator + */ + protected function buildColumnRange(string $lower, string $upper) + { + $upper++; + for ($i = $lower; $i !== $upper; $i++) { + yield $i; + } } /** @@ -269,7 +379,7 @@ protected function appendRow($row, $sheetExport) */ protected function tempFile(): string { - return tempnam($this->tmpPath, 'laravel-excel'); + return $this->tmpPath . DIRECTORY_SEPARATOR . 'laravel-excel-' . str_random(16); } /** @@ -280,4 +390,28 @@ private function hasRows(): bool { return $this->worksheet->cellExists('A1'); } + + /** + * @param object $sheetExport + * + * @return bool + */ + private function hasStrictNullComparison($sheetExport): bool + { + return $sheetExport instanceof WithStrictNullComparison; + } + + /** + * @param object|WithCustomChunkSize $export + * + * @return int + */ + private function getChunkSize($export): int + { + if ($export instanceof WithCustomChunkSize) { + return $export->chunkSize(); + } + + return $this->chunkSize; + } } diff --git a/src/Writer.php b/src/Writer.php index 02d770a6b..4a76dd7fc 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -6,10 +6,12 @@ use PhpOffice\PhpSpreadsheet\Writer\Csv; use Maatwebsite\Excel\Concerns\WithTitle; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use Maatwebsite\Excel\Concerns\WithCharts; use Maatwebsite\Excel\Concerns\WithEvents; use Maatwebsite\Excel\Events\BeforeExport; use Maatwebsite\Excel\Events\BeforeWriting; use Maatwebsite\Excel\Concerns\WithMultipleSheets; +use Maatwebsite\Excel\Concerns\WithCustomCsvSettings; class Writer { @@ -23,7 +25,7 @@ class Writer /** * @var object */ - protected $export; + protected $exportable; /** * @var string @@ -33,32 +35,32 @@ class Writer /** * @var string */ - protected $delimiter; + protected $delimiter = ','; /** * @var string */ - protected $enclosure; + protected $enclosure = '"'; /** * @var string */ - protected $lineEnding; + protected $lineEnding = PHP_EOL; /** * @var bool */ - protected $useBom; + protected $useBom = false; /** * @var bool */ - protected $includeSeparatorLine; + protected $includeSeparatorLine = false; /** * @var bool */ - protected $excelCompatibility; + protected $excelCompatibility = false; /** * @var string @@ -70,13 +72,8 @@ class Writer */ public function __construct() { - $this->tmpPath = config('excel.exports.temp_path', sys_get_temp_dir()); - $this->delimiter = config('excel.exports.csv.delimiter', ','); - $this->enclosure = config('excel.exports.csv.enclosure', '"'); - $this->lineEnding = config('excel.exports.csv.line_ending', PHP_EOL); - $this->useBom = config('excel.exports.csv.use_bom', false); - $this->includeSeparatorLine = config('excel.exports.csv.include_separator_line', false); - $this->excelCompatibility = config('excel.exports.csv.excel_compatibility', false); + $this->tmpPath = config('excel.exports.temp_path', sys_get_temp_dir()); + $this->applyCsvSettings(config('excel.exports.csv', [])); } /** @@ -100,9 +97,7 @@ public function export($export, string $writerType): string $this->addNewSheet()->export($sheetExport); } - $this->raise(new BeforeWriting($this)); - - return $this->write($this->tempFile(), $writerType); + return $this->write($export, $this->tempFile(), $writerType); } /** @@ -112,14 +107,17 @@ public function export($export, string $writerType): string */ public function open($export) { + $this->exportable = $export; + if ($export instanceof WithEvents) { $this->registerListeners($export->registerEvents()); } + $this->exportable = $export; $this->spreadsheet = new Spreadsheet; $this->spreadsheet->disconnectWorksheets(); - $this->raise(new BeforeExport($this)); + $this->raise(new BeforeExport($this, $this->exportable)); if ($export instanceof WithTitle) { $this->spreadsheet->getProperties()->setTitle($export->title()); @@ -144,20 +142,33 @@ public function reopen(string $tempFile, string $writerType) } /** + * @param object $export * @param string $fileName * @param string $writerType * - * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception - * @return string: string + * @return string */ - public function write(string $fileName, string $writerType) + public function write($export, string $fileName, string $writerType) { + $this->exportable = $export; + + $this->raise(new BeforeWriting($this, $this->exportable)); + + if ($export instanceof WithCustomCsvSettings) { + $this->applyCsvSettings($export->getCsvSettings()); + } + $writer = IOFactory::createWriter($this->spreadsheet, $writerType); + if ($export instanceof WithCharts) { + $writer->setIncludeCharts(true); + } + if ($writer instanceof Csv) { $writer->setDelimiter($this->delimiter); $writer->setEnclosure($this->enclosure); $writer->setLineEnding($this->lineEnding); + $writer->setUseBOM($this->useBom); $writer->setIncludeSeparatorLine($this->includeSeparatorLine); $writer->setExcelCompatibility($this->excelCompatibility); } @@ -254,7 +265,7 @@ public function getDelegate() */ public function tempFile(): string { - return tempnam($this->tmpPath, 'laravel-excel'); + return $this->tmpPath . DIRECTORY_SEPARATOR . 'laravel-excel-' . str_random(16); } /** @@ -267,4 +278,27 @@ public function getSheetByIndex(int $sheetIndex) { return new Sheet($this->getDelegate()->getSheet($sheetIndex)); } + + /** + * @param array $config + */ + public function applyCsvSettings(array $config) + { + $this->delimiter = array_get($config, 'delimiter', $this->delimiter); + $this->enclosure = array_get($config, 'enclosure', $this->enclosure); + $this->lineEnding = array_get($config, 'line_ending', $this->lineEnding); + $this->useBom = array_get($config, 'use_bom', $this->useBom); + $this->includeSeparatorLine = array_get($config, 'include_separator_line', $this->includeSeparatorLine); + $this->excelCompatibility = array_get($config, 'excel_compatibility', $this->excelCompatibility); + } + + /** + * @param string $concern + * + * @return bool + */ + public function hasConcern($concern): bool + { + return $this->exportable instanceof $concern; + } } diff --git a/tests/Concerns/FromCollectionTest.php b/tests/Concerns/FromCollectionTest.php index da9e05916..103527569 100644 --- a/tests/Concerns/FromCollectionTest.php +++ b/tests/Concerns/FromCollectionTest.php @@ -2,6 +2,7 @@ namespace Maatwebsite\Excel\Tests\Concerns; +use Illuminate\Support\Collection; use Maatwebsite\Excel\Tests\TestCase; use Maatwebsite\Excel\Tests\Data\Stubs\QueuedExport; use Maatwebsite\Excel\Tests\Data\Stubs\SheetWith100Rows; diff --git a/tests/Concerns/FromViewTest.php b/tests/Concerns/FromViewTest.php index 7226237b4..a8a584880 100644 --- a/tests/Concerns/FromViewTest.php +++ b/tests/Concerns/FromViewTest.php @@ -7,7 +7,9 @@ use Maatwebsite\Excel\Tests\TestCase; use Maatwebsite\Excel\Concerns\FromView; use Maatwebsite\Excel\Concerns\Exportable; +use Maatwebsite\Excel\Concerns\WithMultipleSheets; use Maatwebsite\Excel\Tests\Data\Stubs\Database\User; +use Maatwebsite\Excel\Tests\Data\Stubs\SheetForUsersFromView; class FromViewTest extends TestCase { @@ -71,4 +73,70 @@ public function view(): View $this->assertEquals($expected, $contents); } + + /** + * @test + */ + public function can_export_multiple_sheets_from_view() + { + /** @var Collection|User[] $users */ + $users = factory(User::class)->times(300)->make(); + + $export = new class($users) implements WithMultipleSheets { + use Exportable; + + /** + * @var Collection + */ + protected $users; + + /** + * @param Collection $users + */ + public function __construct(Collection $users) + { + $this->users = $users; + } + + /** + * @return SheetForUsersFromView[] + */ + public function sheets() : array + { + return [ + new SheetForUsersFromView($this->users->forPage(1, 100)), + new SheetForUsersFromView($this->users->forPage(2, 100)), + new SheetForUsersFromView($this->users->forPage(3, 100)), + ]; + } + }; + + $response = $export->store('from-multiple-view.xlsx'); + + $this->assertTrue($response); + + $contents = $this->readAsArray(__DIR__ . '/../Data/Disks/Local/from-multiple-view.xlsx', 'Xlsx', 0); + + $expected = $users->forPage(1, 100)->map(function (User $user) { + return [ + $user->name, + $user->email, + ]; + })->prepend(['Name', 'Email'])->toArray(); + + $this->assertEquals(101, sizeof($contents)); + $this->assertEquals($expected, $contents); + + $contents = $this->readAsArray(__DIR__ . '/../Data/Disks/Local/from-multiple-view.xlsx', 'Xlsx', 2); + + $expected = $users->forPage(3, 100)->map(function (User $user) { + return [ + $user->name, + $user->email, + ]; + })->prepend(['Name', 'Email'])->toArray(); + + $this->assertEquals(101, sizeof($contents)); + $this->assertEquals($expected, $contents); + } } diff --git a/tests/Concerns/WithCustomCsvSettingsTest.php b/tests/Concerns/WithCustomCsvSettingsTest.php new file mode 100644 index 000000000..c29567835 --- /dev/null +++ b/tests/Concerns/WithCustomCsvSettingsTest.php @@ -0,0 +1,66 @@ +SUT = $this->app->make(Excel::class); + } + + /** + * @test + */ + public function can_store_csv_export_with_custom_settings() + { + $export = new class implements FromCollection, WithCustomCsvSettings { + /** + * @return Collection + */ + public function collection() + { + return collect([ + ['A1', 'B1'], + ['A2', 'B2'], + ]); + } + + /** + * @return array + */ + public function getCsvSettings(): array + { + return [ + 'delimiter' => ';', + 'enclosure' => '', + 'line_ending' => PHP_EOL, + 'use_bom' => true, + 'include_separator_line' => true, + 'excel_compatibility' => false, + ]; + } + }; + + $this->SUT->store($export, 'custom-csv.csv'); + + $contents = file_get_contents(__DIR__ . '/../Data/Disks/Local/custom-csv.csv'); + + $this->assertContains('sep=;', $contents); + $this->assertContains('A1;B1', $contents); + $this->assertContains('A2;B2', $contents); + } +} diff --git a/tests/Concerns/WithCustomQuerySizeTest.php b/tests/Concerns/WithCustomQuerySizeTest.php new file mode 100644 index 000000000..f49e63f2e --- /dev/null +++ b/tests/Concerns/WithCustomQuerySizeTest.php @@ -0,0 +1,58 @@ +loadLaravelMigrations(['--database' => 'testing']); + $this->loadMigrationsFrom(dirname(__DIR__) . '/Data/Stubs/Database/Migrations'); + $this->withFactories(dirname(__DIR__) . '/Data/Stubs/Database/Factories'); + + factory(Group::class)->times(5)->create()->each(function ($group) { + $group->users()->attach(factory(User::class)->times(rand(1, 3))->create()); + }); + + config()->set('excel.exports.chunk_size', 2); + } + + /** + * @test + */ + public function can_export_with_custom_count() + { + $export = new FromQueryWithCustomQuerySize(); + + $export->queue('export-from-query-with-count.xlsx', null, 'Xlsx')->chain([ + new AfterQueueExportJob(dirname(__DIR__) . '/Data/Disks/Local/export-from-query-with-count.xlsx'), + ]); + + $actual = $this->readAsArray(dirname(__DIR__) . '/Data/Disks/Local/export-from-query-with-count.xlsx', 'Xlsx'); + + $this->assertCount(Group::count(), $actual); + } + + /** + * {@inheritdoc} + */ + protected function getPackageProviders($app) + { + return [ + \Orchestra\Database\ConsoleServiceProvider::class, + ]; + + return parent::getPackageAliases($app); + } +} diff --git a/tests/Concerns/WithCustomStartCellTest.php b/tests/Concerns/WithCustomStartCellTest.php new file mode 100644 index 000000000..42e98ff0b --- /dev/null +++ b/tests/Concerns/WithCustomStartCellTest.php @@ -0,0 +1,61 @@ +SUT = $this->app->make(Excel::class); + } + + /** + * @test + */ + public function can_store_collection_with_custom_start_cell() + { + $export = new class implements FromCollection, WithCustomStartCell { + /** + * @return Collection + */ + public function collection() + { + return collect([ + ['A1', 'B1'], + ['A2', 'B2'], + ]); + } + + /** + * @return string + */ + public function startCell(): string + { + return 'B2'; + } + }; + + $this->SUT->store($export, 'custom-start-cell.csv'); + + $contents = $this->readAsArray(__DIR__ . '/../Data/Disks/Local/custom-start-cell.csv', 'Csv'); + + $this->assertEquals([ + [null, null, null], + [null, 'A1', 'B1'], + [null, 'A2', 'B2'], + ], $contents); + } +} diff --git a/tests/Concerns/WithEventsTest.php b/tests/Concerns/WithEventsTest.php index f05c25e82..daf813d1b 100644 --- a/tests/Concerns/WithEventsTest.php +++ b/tests/Concerns/WithEventsTest.php @@ -2,15 +2,19 @@ namespace Maatwebsite\Excel\Tests\Concerns; +use Maatwebsite\Excel\Excel; use Maatwebsite\Excel\Sheet; use Maatwebsite\Excel\Writer; use Maatwebsite\Excel\Tests\TestCase; use Maatwebsite\Excel\Events\AfterSheet; use Maatwebsite\Excel\Events\BeforeSheet; +use Maatwebsite\Excel\Concerns\Exportable; use Maatwebsite\Excel\Events\BeforeExport; use Maatwebsite\Excel\Events\BeforeWriting; +use Maatwebsite\Excel\Tests\Data\Stubs\CustomConcern; use Maatwebsite\Excel\Tests\Data\Stubs\ExportWithEvents; use Symfony\Component\HttpFoundation\BinaryFileResponse; +use Maatwebsite\Excel\Tests\Data\Stubs\CustomSheetConcern; use Maatwebsite\Excel\Tests\Data\Stubs\BeforeExportListener; class WithEventsTest extends TestCase @@ -66,4 +70,119 @@ public function can_have_invokable_class_as_listener() $this->assertInstanceOf(BinaryFileResponse::class, $event->download('filename.xlsx')); } + + /** + * @test + */ + public function can_have_global_event_listeners() + { + $event = new class { + use Exportable; + }; + + $beforeExport = false; + Writer::listen(BeforeExport::class, function () use (&$beforeExport) { + $beforeExport = true; + }); + + $beforeWriting = false; + Writer::listen(BeforeWriting::class, function () use (&$beforeWriting) { + $beforeWriting = true; + }); + + $beforeSheet = false; + Sheet::listen(BeforeSheet::class, function () use (&$beforeSheet) { + $beforeSheet = true; + }); + + $afterSheet = false; + Sheet::listen(AfterSheet::class, function () use (&$afterSheet) { + $afterSheet = true; + }); + + $this->assertInstanceOf(BinaryFileResponse::class, $event->download('filename.xlsx')); + + $this->assertTrue($beforeExport, 'Before export event not triggered'); + $this->assertTrue($beforeWriting, 'Before writing event not triggered'); + $this->assertTrue($beforeSheet, 'Before sheet event not triggered'); + $this->assertTrue($afterSheet, 'After sheet event not triggered'); + } + + /** + * @test + */ + public function can_have_custom_concern_handlers() + { + // Add a custom concern handler for the given concern. + Excel::extend(CustomConcern::class, function (CustomConcern $exportable, Writer $writer) { + $writer->getSheetByIndex(0)->append( + $exportable->custom() + ); + }); + + $exportWithConcern = new class implements CustomConcern { + use Exportable; + + public function custom() + { + return [ + ['a', 'b'], + ]; + } + }; + + $exportWithConcern->store('with-custom-concern.xlsx'); + $actual = $this->readAsArray(__DIR__ . '/../Data/Disks/Local/with-custom-concern.xlsx', 'Xlsx'); + $this->assertEquals([ + ['a', 'b'], + ], $actual); + + $exportWithoutConcern = new class { + use Exportable; + }; + + $exportWithoutConcern->store('without-custom-concern.xlsx'); + $actual = $this->readAsArray(__DIR__ . '/../Data/Disks/Local/without-custom-concern.xlsx', 'Xlsx'); + + $this->assertEquals([[null]], $actual); + } + + /** + * @test + */ + public function can_have_custom_sheet_concern_handlers() + { + // Add a custom concern handler for the given concern. + Excel::extend(CustomSheetConcern::class, function (CustomSheetConcern $exportable, Sheet $sheet) { + $sheet->append( + $exportable->custom() + ); + }, AfterSheet::class); + + $exportWithConcern = new class implements CustomSheetConcern { + use Exportable; + + public function custom() + { + return [ + ['c', 'd'], + ]; + } + }; + + $exportWithConcern->store('with-custom-concern.xlsx'); + $actual = $this->readAsArray(__DIR__ . '/../Data/Disks/Local/with-custom-concern.xlsx', 'Xlsx'); + $this->assertEquals([ + ['c', 'd'], + ], $actual); + + $exportWithoutConcern = new class { + use Exportable; + }; + + $exportWithoutConcern->store('without-custom-concern.xlsx'); + $actual = $this->readAsArray(__DIR__ . '/../Data/Disks/Local/without-custom-concern.xlsx', 'Xlsx'); + + $this->assertEquals([[null]], $actual); + } } diff --git a/tests/Concerns/WithMultipleSheetsTest.php b/tests/Concerns/WithMultipleSheetsTest.php new file mode 100644 index 000000000..59f98861e --- /dev/null +++ b/tests/Concerns/WithMultipleSheetsTest.php @@ -0,0 +1,96 @@ +withFactories(__DIR__ . '/../Data/Stubs/Database/Factories'); + } + + /** + * @test + */ + public function can_export_with_multiple_sheets_using_collections() + { + $export = new class implements WithMultipleSheets { + use Exportable; + + /** + * @return SheetWith100Rows[] + */ + public function sheets() : array + { + return [ + new SheetWith100Rows('A'), + new SheetWith100Rows('B'), + new SheetWith100Rows('C'), + ]; + } + }; + + $export->store('from-view.xlsx'); + + $this->assertCount(100, $this->readAsArray(__DIR__ . '/../Data/Disks/Local/from-view.xlsx', 'Xlsx', 0)); + $this->assertCount(100, $this->readAsArray(__DIR__ . '/../Data/Disks/Local/from-view.xlsx', 'Xlsx', 1)); + $this->assertCount(100, $this->readAsArray(__DIR__ . '/../Data/Disks/Local/from-view.xlsx', 'Xlsx', 2)); + } + + /** + * @test + */ + public function can_export_multiple_sheets_from_view() + { + /** @var Collection|User[] $users */ + $users = factory(User::class)->times(300)->make(); + + $export = new class($users) implements WithMultipleSheets { + use Exportable; + + /** + * @var Collection + */ + protected $users; + + /** + * @param Collection $users + */ + public function __construct(Collection $users) + { + $this->users = $users; + } + + /** + * @return SheetForUsersFromView[] + */ + public function sheets() : array + { + return [ + new SheetForUsersFromView($this->users->forPage(1, 100)), + new SheetForUsersFromView($this->users->forPage(2, 100)), + new SheetForUsersFromView($this->users->forPage(3, 100)), + ]; + } + }; + + $export->store('from-view.xlsx'); + + $this->assertCount(101, $this->readAsArray(__DIR__ . '/../Data/Disks/Local/from-view.xlsx', 'Xlsx', 0)); + $this->assertCount(101, $this->readAsArray(__DIR__ . '/../Data/Disks/Local/from-view.xlsx', 'Xlsx', 1)); + $this->assertCount(101, $this->readAsArray(__DIR__ . '/../Data/Disks/Local/from-view.xlsx', 'Xlsx', 2)); + } +} diff --git a/tests/Concerns/WithStrictNullComparisonTest.php b/tests/Concerns/WithStrictNullComparisonTest.php new file mode 100644 index 000000000..389237b66 --- /dev/null +++ b/tests/Concerns/WithStrictNullComparisonTest.php @@ -0,0 +1,95 @@ +store('with-strict-null-comparison-store.xlsx'); + + $this->assertTrue($response); + + $actual = $this->readAsArray(__DIR__ . '/../Data/Disks/Local/with-strict-null-comparison-store.xlsx', 'Xlsx'); + + $expected = [ + ['string', 0.0, 0.0, 0.0, 'string'], + ['string', 0.0, 0.0, 0.0, 'string'], + ]; + + $this->assertEquals($expected, $actual); + } + + /** + * @test + */ + public function exported_zero_values_are_null_when_not_exporting_with_strict_null_comparison() + { + $export = new class implements FromCollection, WithHeadings { + use Exportable; + + /** + * @return Collection + */ + public function collection() + { + return collect([ + ['string', 0, 0.0, 'string'], + ]); + } + + /** + * @return array + */ + public function headings(): array + { + return ['string', 0, 0.0, 'string']; + } + }; + + $response = $export->store('without-strict-null-comparison-store.xlsx'); + + $this->assertTrue($response); + + $actual = $this->readAsArray(__DIR__ . '/../Data/Disks/Local/without-strict-null-comparison-store.xlsx', 'Xlsx'); + + $expected = [ + ['string', null, null, 'string'], + ['string', null, null, 'string'], + ]; + + $this->assertEquals($expected, $actual); + } +} diff --git a/tests/Data/Stubs/CustomConcern.php b/tests/Data/Stubs/CustomConcern.php new file mode 100644 index 000000000..2ee194177 --- /dev/null +++ b/tests/Data/Stubs/CustomConcern.php @@ -0,0 +1,8 @@ +define(Group::class, function (Faker $faker) { + return [ + 'name' => $faker->word, + ]; +}); diff --git a/tests/Data/Stubs/Database/Group.php b/tests/Data/Stubs/Database/Group.php new file mode 100644 index 000000000..0042c4a54 --- /dev/null +++ b/tests/Data/Stubs/Database/Group.php @@ -0,0 +1,16 @@ +belongsToMany(User::class); + } +} diff --git a/tests/Data/Stubs/Database/Migrations/0000_00_00_000000_create_groups_table.php b/tests/Data/Stubs/Database/Migrations/0000_00_00_000000_create_groups_table.php new file mode 100644 index 000000000..12a744976 --- /dev/null +++ b/tests/Data/Stubs/Database/Migrations/0000_00_00_000000_create_groups_table.php @@ -0,0 +1,28 @@ +increments('id'); + $table->string('name'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down() + { + Schema::dropIfExists('groups'); + } +} diff --git a/tests/Data/Stubs/Database/Migrations/0000_00_00_000001_create_group_user_table.php b/tests/Data/Stubs/Database/Migrations/0000_00_00_000001_create_group_user_table.php new file mode 100644 index 000000000..96c8d6919 --- /dev/null +++ b/tests/Data/Stubs/Database/Migrations/0000_00_00_000001_create_group_user_table.php @@ -0,0 +1,38 @@ +increments('id'); + $table->unsignedInteger('group_id'); + $table->unsignedInteger('user_id'); + + $table->foreign('user_id') + ->references('id') + ->on('users') + ->onDelete('cascade'); + + $table->foreign('group_id') + ->references('id') + ->on('groups') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down() + { + Schema::dropIfExists('group_user'); + } +} diff --git a/tests/Data/Stubs/Database/User.php b/tests/Data/Stubs/Database/User.php index 60c1ade86..7ce7a0ec3 100644 --- a/tests/Data/Stubs/Database/User.php +++ b/tests/Data/Stubs/Database/User.php @@ -6,5 +6,13 @@ class User extends Model { + /** + * @var array + */ protected $guarded = []; + + /** + * @var array + */ + protected $hidden = ['password', 'email_verified_at']; } diff --git a/tests/Data/Stubs/EloquentCollectionWithMappingExport.php b/tests/Data/Stubs/EloquentCollectionWithMappingExport.php new file mode 100644 index 000000000..ed4447a06 --- /dev/null +++ b/tests/Data/Stubs/EloquentCollectionWithMappingExport.php @@ -0,0 +1,40 @@ + 'Patrick', + 'lastname' => 'Brouwers', + ]), + ]); + } + + /** + * @param User $user + * + * @return array + */ + public function map($user): array + { + return [ + $user->firstname, + $user->lastname, + ]; + } +} diff --git a/tests/Data/Stubs/FromQueryWithCustomQuerySize.php b/tests/Data/Stubs/FromQueryWithCustomQuerySize.php new file mode 100644 index 000000000..9ab875fa1 --- /dev/null +++ b/tests/Data/Stubs/FromQueryWithCustomQuerySize.php @@ -0,0 +1,53 @@ +join('group_user', 'groups.id', '=', 'group_user.group_id') + ->select('groups.*', DB::raw('count(group_user.user_id) as number_of_users')) + ->groupBy('groups.id') + ->orderBy('number_of_users'); + + return $query; + } + + /** + * @return int + */ + public function querySize(): int + { + return Group::has('users')->count(); + } + + /** + * @param Group $row + * + * @return array + */ + public function map($row): array + { + return [ + $row->id, + $row->name, + $row->number_of_users, + ]; + } +} diff --git a/tests/Data/Stubs/FromUsersQueryExport.php b/tests/Data/Stubs/FromUsersQueryExport.php index 17b013ef9..93494af52 100644 --- a/tests/Data/Stubs/FromUsersQueryExport.php +++ b/tests/Data/Stubs/FromUsersQueryExport.php @@ -4,14 +4,11 @@ use Illuminate\Database\Query\Builder; use Maatwebsite\Excel\Concerns\FromQuery; -use Maatwebsite\Excel\Events\BeforeSheet; use Maatwebsite\Excel\Concerns\Exportable; -use Maatwebsite\Excel\Concerns\WithEvents; -use Illuminate\Contracts\Support\Arrayable; -use Maatwebsite\Excel\Concerns\WithMapping; +use Maatwebsite\Excel\Concerns\WithCustomChunkSize; use Maatwebsite\Excel\Tests\Data\Stubs\Database\User; -class FromUsersQueryExport implements FromQuery, WithMapping, WithEvents +class FromUsersQueryExport implements FromQuery, WithCustomChunkSize { use Exportable; @@ -24,24 +21,10 @@ public function query() } /** - * @param mixed $row - * - * @return array + * @return int */ - public function map($row): array + public function chunkSize(): int { - return $row instanceof Arrayable ? $row->toArray() : $row; - } - - /** - * @return array - */ - public function registerEvents(): array - { - return [ - BeforeSheet::class => function (BeforeSheet $event) { - $event->sheet->chunkSize(10); - }, - ]; + return 10; } } diff --git a/tests/Data/Stubs/FromUsersQueryExportWithMapping.php b/tests/Data/Stubs/FromUsersQueryExportWithMapping.php new file mode 100644 index 000000000..c1a37cf71 --- /dev/null +++ b/tests/Data/Stubs/FromUsersQueryExportWithMapping.php @@ -0,0 +1,48 @@ + function (BeforeSheet $event) { + $event->sheet->chunkSize(10); + }, + ]; + } + + /** + * @param User $row + * + * @return array + */ + public function map($row): array + { + return [ + 'name' => $row->name, + ]; + } +} diff --git a/tests/Data/Stubs/SheetForUsersFromView.php b/tests/Data/Stubs/SheetForUsersFromView.php new file mode 100644 index 000000000..922d11acc --- /dev/null +++ b/tests/Data/Stubs/SheetForUsersFromView.php @@ -0,0 +1,36 @@ +users = $users; + } + + /** + * @return View + */ + public function view(): View + { + return view('users', [ + 'users' => $this->users, + ]); + } +} diff --git a/tests/ExcelTest.php b/tests/ExcelTest.php index 7494b99c4..243450d73 100644 --- a/tests/ExcelTest.php +++ b/tests/ExcelTest.php @@ -39,7 +39,7 @@ public function can_download_an_export_object_with_facade() $response = ExcelFacade::download($export, 'filename.xlsx'); $this->assertInstanceOf(BinaryFileResponse::class, $response); - $this->assertEquals('attachment; filename="filename.xlsx"', $response->headers->get('Content-Disposition')); + $this->assertEquals('attachment; filename=filename.xlsx', str_replace('"', '', $response->headers->get('Content-Disposition'))); } /** @@ -52,7 +52,7 @@ public function can_download_an_export_object() $response = $this->SUT->download($export, 'filename.xlsx'); $this->assertInstanceOf(BinaryFileResponse::class, $response); - $this->assertEquals('attachment; filename="filename.xlsx"', $response->headers->get('Content-Disposition')); + $this->assertEquals('attachment; filename=filename.xlsx', str_replace('"', '', $response->headers->get('Content-Disposition'))); } /** @@ -94,6 +94,19 @@ public function can_store_csv_export_with_default_settings() $this->assertFileExists(__DIR__ . '/Data/Disks/Local/filename.csv'); } + /** + * @test + */ + public function can_store_tsv_export_with_default_settings() + { + $export = new EmptyExport; + + $response = $this->SUT->store($export, 'filename.tsv'); + + $this->assertTrue($response); + $this->assertFileExists(__DIR__ . '/Data/Disks/Local/filename.tsv'); + } + /** * @test */ diff --git a/tests/Mixins/DownloadCollectionTest.php b/tests/Mixins/DownloadCollectionTest.php index 14a86b6d4..ef3a45c7c 100644 --- a/tests/Mixins/DownloadCollectionTest.php +++ b/tests/Mixins/DownloadCollectionTest.php @@ -2,8 +2,10 @@ namespace Maatwebsite\Excel\Tests\Mixins; +use Maatwebsite\Excel\Excel; use Illuminate\Support\Collection; use Maatwebsite\Excel\Tests\TestCase; +use Maatwebsite\Excel\Tests\Data\Stubs\Database\User; use Symfony\Component\HttpFoundation\BinaryFileResponse; class DownloadCollectionTest extends TestCase @@ -11,19 +13,77 @@ class DownloadCollectionTest extends TestCase /** * @test */ - public function can_store_a_collection_as_excel() + public function can_download_a_collection_as_excel() { $collection = new Collection([ - ['test', 'test'], - ['test', 'test'], + ['column_1' => 'test', 'column_2' => 'test'], + ['column_1' => 'test2', 'column_2' => 'test2'], ]); - $response = $collection->downloadExcel('collection-download.xlsx'); + $response = $collection->downloadExcel('collection-download.xlsx', Excel::XLSX); + + $array = $this->readAsArray($response->getFile()->getPathName(), Excel::XLSX); + + // First row are not headings + $firstRow = collect($array)->first(); + $this->assertEquals(['test', 'test'], $firstRow); $this->assertInstanceOf(BinaryFileResponse::class, $response); $this->assertEquals( - 'attachment; filename="collection-download.xlsx"', - $response->headers->get('Content-Disposition') + 'attachment; filename=collection-download.xlsx', + str_replace('"', '', $response->headers->get('Content-Disposition')) ); } + + /** + * @test + */ + public function can_download_a_collection_with_headers_as_excel() + { + $collection = new Collection([ + ['column_1' => 'test', 'column_2' => 'test'], + ['column_1' => 'test', 'column_2' => 'test'], + ]); + + $response = $collection->downloadExcel('collection-headers-download.xlsx', Excel::XLSX, true); + + $array = $this->readAsArray($response->getFile()->getPathName(), Excel::XLSX); + + $this->assertEquals(['column_1', 'column_2'], collect($array)->first()); + } + + /** + * @test + */ + public function can_download_collection_with_headers_with_hidden_eloquent_attributes() + { + $collection = new Collection([ + new User(['name' => 'Patrick', 'password' => 'my_password']), + ]); + + $response = $collection->downloadExcel('collection-headers-download.xlsx', Excel::XLSX, true); + + $array = $this->readAsArray($response->getFile()->getPathName(), Excel::XLSX); + + $this->assertEquals(['name'], collect($array)->first()); + } + + /** + * @test + */ + public function can_download_collection_with_headers_when_making_attributes_visible() + { + $user = new User(['name' => 'Patrick', 'password' => 'my_password']); + $user->makeVisible(['password']); + + $collection = new Collection([ + $user, + ]); + + $response = $collection->downloadExcel('collection-headers-download.xlsx', Excel::XLSX, true); + + $array = $this->readAsArray($response->getFile()->getPathName(), Excel::XLSX); + + $this->assertEquals(['name', 'password'], collect($array)->first()); + } } diff --git a/tests/Mixins/StoreCollectionTest.php b/tests/Mixins/StoreCollectionTest.php index b647b22c8..84a15937f 100644 --- a/tests/Mixins/StoreCollectionTest.php +++ b/tests/Mixins/StoreCollectionTest.php @@ -2,6 +2,7 @@ namespace Maatwebsite\Excel\Tests\Mixins; +use Maatwebsite\Excel\Excel; use Illuminate\Support\Collection; use Maatwebsite\Excel\Tests\TestCase; @@ -29,13 +30,52 @@ public function can_store_a_collection_as_excel() public function can_store_a_collection_as_excel_on_non_default_disk() { $collection = new Collection([ + ['column_1' => 'test', 'column_2' => 'test'], + ['column_1' => 'test2', 'column_2' => 'test2'], + ]); + + $response = $collection->storeExcel('collection-store.xlsx', null, Excel::XLSX); + + $file = __DIR__ . '/../Data/Disks/Local/collection-store.xlsx'; + + $this->assertTrue($response); + $this->assertFileExists($file); + + $array = $this->readAsArray($file, Excel::XLSX); + + // First row are not headings + $firstRow = collect($array)->first(); + $this->assertEquals(['test', 'test'], $firstRow); + + $this->assertEquals([ ['test', 'test'], - ['test', 'test'], + ['test2', 'test2'], + ], collect($array)->values()->all()); + } + + /** + * @test + */ + public function can_store_a_collection_with_headings_as_excel() + { + $collection = new Collection([ + ['column_1' => 'test', 'column_2' => 'test'], + ['column_1' => 'test', 'column_2' => 'test'], ]); - $response = $collection->storeExcel('collection-store.xlsx', 'test'); + $response = $collection->storeExcel('collection-headers-store.xlsx', null, Excel::XLSX, true); + + $file = __DIR__ . '/../Data/Disks/Local/collection-headers-store.xlsx'; $this->assertTrue($response); - $this->assertFileExists(__DIR__ . '/../Data/Disks/Test/collection-store.xlsx'); + $this->assertFileExists($file); + + $array = $this->readAsArray($file, Excel::XLSX); + $this->assertEquals(['column_1', 'column_2'], collect($array)->first()); + + $this->assertEquals([ + ['test', 'test'], + ['test', 'test'], + ], collect($array)->except(0)->values()->all()); } } diff --git a/tests/QueuedExportTest.php b/tests/QueuedExportTest.php index eefcbc93f..f3966b561 100644 --- a/tests/QueuedExportTest.php +++ b/tests/QueuedExportTest.php @@ -5,6 +5,7 @@ use Maatwebsite\Excel\Tests\Data\Stubs\QueuedExport; use Maatwebsite\Excel\Tests\Data\Stubs\ShouldQueueExport; use Maatwebsite\Excel\Tests\Data\Stubs\AfterQueueExportJob; +use Maatwebsite\Excel\Tests\Data\Stubs\EloquentCollectionWithMappingExport; class QueuedExportTest extends TestCase { @@ -43,4 +44,22 @@ public function can_implicitly_queue_an_export() new AfterQueueExportJob(__DIR__ . '/Data/Disks/Test/queued-export.xlsx'), ]); } + + /** + * @test + */ + public function can_queue_export_with_mapping_on_eloquent_models() + { + $export = new EloquentCollectionWithMappingExport(); + + $export->queue('queued-export.xlsx')->chain([ + new AfterQueueExportJob(__DIR__ . '/Data/Disks/Local/queued-export.xlsx'), + ]); + + $actual = $this->readAsArray(__DIR__ . '/Data/Disks/Local/queued-export.xlsx', 'Xlsx'); + + $this->assertEquals([ + ['Patrick', 'Brouwers'], + ], $actual); + } } diff --git a/tests/QueuedQueryExportTest.php b/tests/QueuedQueryExportTest.php index 80ba36e2d..8ca9fc216 100644 --- a/tests/QueuedQueryExportTest.php +++ b/tests/QueuedQueryExportTest.php @@ -5,6 +5,7 @@ use Maatwebsite\Excel\Tests\Data\Stubs\Database\User; use Maatwebsite\Excel\Tests\Data\Stubs\AfterQueueExportJob; use Maatwebsite\Excel\Tests\Data\Stubs\FromUsersQueryExport; +use Maatwebsite\Excel\Tests\Data\Stubs\FromUsersQueryExportWithMapping; class QueuedQueryExportTest extends TestCase { @@ -35,5 +36,28 @@ public function can_queue_an_export() $actual = $this->readAsArray(__DIR__ . '/Data/Disks/Local/queued-query-export.xlsx', 'Xlsx'); $this->assertCount(100, $actual); + + // 6 of the 7 columns in export, excluding the "hidden" password column. + $this->assertCount(6, $actual[0]); + } + + /** + * @test + */ + public function can_queue_an_export_with_mapping() + { + $export = new FromUsersQueryExportWithMapping(); + + $export->queue('queued-query-export-with-mapping.xlsx')->chain([ + new AfterQueueExportJob(__DIR__ . '/Data/Disks/Local/queued-query-export-with-mapping.xlsx'), + ]); + + $actual = $this->readAsArray(__DIR__ . '/Data/Disks/Local/queued-query-export-with-mapping.xlsx', 'Xlsx'); + + $this->assertCount(100, $actual); + + // Only 1 column when using map() + $this->assertCount(1, $actual[0]); + $this->assertEquals(User::value('name'), $actual[0][0]); } }