├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── config └── mckue-excel.php ├── phpunit.xml.dist ├── pint.json ├── pre-commit.sample ├── src ├── Commands │ └── InfoCommand.php ├── Concerns │ ├── Exportable.php │ ├── FromArray.php │ ├── FromCollection.php │ ├── FromGenerator.php │ ├── FromIterator.php │ ├── FromQuery.php │ ├── FromView.php │ ├── Importable.php │ ├── OnEachRow.php │ ├── RegistersEventListeners.php │ ├── ShouldAutoSize.php │ ├── SkipsEmptyRows.php │ ├── SkipsErrors.php │ ├── SkipsFailures.php │ ├── SkipsOnError.php │ ├── SkipsOnFailure.php │ ├── SkipsUnknownSheets.php │ ├── ToArray.php │ ├── ToCollection.php │ ├── ToModel.php │ ├── WithBackgroundColor.php │ ├── WithBatchInserts.php │ ├── WithCalculatedFormulas.php │ ├── WithChunkReading.php │ ├── WithColumnFormatting.php │ ├── WithColumnLimit.php │ ├── WithCustomChunkSize.php │ ├── WithEvents.php │ ├── WithFormatData.php │ ├── WithHeadingRow.php │ ├── WithHeadings.php │ ├── WithLimit.php │ ├── WithMappedCells.php │ ├── WithMapping.php │ ├── WithMergeCells.php │ ├── WithMultipleSheets.php │ ├── WithProperties.php │ ├── WithRowFormatting.php │ ├── WithStartRow.php │ ├── WithStyles.php │ ├── WithTitle.php │ ├── WithUpsertColumns.php │ ├── WithUpserts.php │ └── WithValidation.php ├── DelegatedMacroable.php ├── Events │ ├── AfterImport.php │ ├── AfterSheet.php │ ├── BeforeExport.php │ ├── BeforeImport.php │ ├── BeforeSheet.php │ ├── BeforeWriting.php │ ├── Event.php │ └── ImportFailed.php ├── Excel.php ├── ExcelServiceProvider.php ├── Exceptions │ ├── ConcernConflictException.php │ ├── LaravelExcelException.php │ ├── NoFilePathGivenException.php │ ├── NoFilenameGivenException.php │ ├── NoTypeDetectedException.php │ ├── RowSkippedException.php │ ├── SheetNotFoundException.php │ └── UnreadableFileException.php ├── Exporter.php ├── Facades │ └── Excel.php ├── Files │ ├── Disk.php │ ├── Filesystem.php │ ├── LocalTemporaryFile.php │ ├── RemoteTemporaryFile.php │ ├── TemporaryFile.php │ └── TemporaryFileFactory.php ├── HasEventBus.php ├── Helpers │ ├── ArrayHelper.php │ ├── CellHelper.php │ └── FileTypeDetector.php ├── Importer.php ├── Imports │ ├── HeadingRowExtractor.php │ ├── ModelImporter.php │ └── ModelManager.php ├── MappedReader.php ├── Mixins │ ├── DownloadCollection.php │ └── StoreCollection.php ├── Reader.php ├── Sheet.php ├── Transactions │ ├── DbTransactionHandler.php │ ├── NullTransactionHandler.php │ ├── TransactionHandler.php │ └── TransactionManager.php ├── Validators │ ├── Failure.php │ ├── RowValidator.php │ └── ValidationException.php └── Writer.php └── tests ├── ExcelTest.php ├── ExportTest.php ├── ImportTest.php └── TestCase.php /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | /vendor 3 | /coverage 4 | sftp-config.json 5 | composer.lock 6 | .subsplit 7 | .php_cs.cache 8 | /*.phar 9 | /.idea 10 | /.phpunit.cache/test-results 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "{}" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright 2019 zlt2000 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Laravel-excel 一款基于xlswriter的laravel扩展包 2 | [![Latest Stable Version](https://poser.pugx.org/mckue/laravel-excel/v/stable)](https://packagist.org/packages/mckue/laravel-excel) 3 | [![Total Downloads](https://poser.pugx.org/mckue/laravel-excel/downloads)](https://packagist.org/packages/mckue/laravel-excel) 4 | [![Latest Stable Version](http://poser.pugx.org/mckue/laravel-excel/v)](https://packagist.org/packages/mckue/laravel-excel) 5 | [![PHP Version Require](http://poser.pugx.org/mckue/laravel-excel/require/php)](https://packagist.org/packages/mckue/laravel-excel) 6 | [![License](https://poser.pugx.org/mckue/laravel-excel/license)](https://packagist.org/packages/mckue/laravel-excel) 7 | 8 | xlswriter是一款高性能的php excel读写扩展,Laravel-excel基于[SpartnerNL/Laravel-Excel](https://github.com/SpartnerNL/Laravel-Excel)代码上,切换成xlswriter扩展。 9 | 如果您的项目使用的是[SpartnerNL/Laravel-Excel](https://github.com/SpartnerNL/Laravel-Excel)并且出现大数据导出性能问题,你不想修改大量的代码,那么当前的包可能会很适合你。 10 | 当然目前的包不可能百分之百兼容所有功能,目前只实现了部分基础的功能。 11 | 12 | [Xlswriter文档](https://xlswriter-docs.viest.me/zh-cn) 13 | 14 | #### 如果本扩展帮助到了你 欢迎star。 15 | 16 | #### 如果本扩展有任何问题或有其他想法 欢迎提 issue与pull request。 17 | 18 | ### Laravel-excel使用教程 19 | #### 环境要求 20 | - `xlswriter` >= 1.3.7 21 | - `PHP` >= 8.0 22 | 安装请按照`XlsWriter`的官方文档:[安装教程](https://xlswriter-docs.viest.me/zh-cn/an-zhuang) 23 | 24 | #### 安装 25 | ``` 26 | composer require mckue/laravel-excel 27 | ``` 28 | 29 | 发布`mckue-excel.php`配置文件: 30 | ``` 31 | php artisan vendor:publish --provider="Mckue\Excel\ExcelServiceProvider" --tag=config 32 | ``` 33 | #### 1.命令 34 | ##### 1.1 查看xlswriter扩展是否正常安装 35 | ``` 36 | php artisan php-ext-xlswriter:status 37 | ``` 38 | 展示信息如下: 39 | ``` 40 | info: 41 | +---------+---------------------------------------------+ 42 | | version | 1.0 | 43 | | author | mckue | 44 | | docs | https://github.com/carlin-rj/laravel-excel | 45 | +---------+---------------------------------------------+ 46 | XlsWriter extension status: 47 | +-------------------------------+----------------------------+ 48 | | loaded | yes | 49 | | xlsWriter author | Jiexing.Wang (wjx@php.net) | 50 | | xlswriter support | enabled | 51 | | Version | 1.3.7 | 52 | | bundled libxlsxwriter version | 1.0.0 | 53 | | bundled libxlsxio version | 0.2.27 | 54 | +-------------------------------+----------------------------+ 55 | 56 | ``` 57 | 如您的信息展示如上所示,证明您的`cli`环境下本扩展可用。 58 | 59 | ### 1.快速开始 60 | ``` 61 | 'True']); 139 | } 140 | ``` 141 | 或者将其存储在磁盘上(例如 s3): 142 | ``` 143 | public function storeExcel() 144 | { 145 | return Excel::store(new InvoicesExport, 'invoices.xlsx', 's3'); 146 | } 147 | ``` 148 | 149 | ### 3.使用自定义结构 150 | ``` 151 | namespace App\Exports; 152 | 153 | use App\Invoice; 154 | use Mckue\Excel\Concerns\FromCollection; 155 | 156 | class InvoicesExport implements FromCollection 157 | { 158 | public function collection() 159 | { 160 | return new Collection([ 161 | [1, 2, 3], 162 | [4, 5, 6] 163 | ]); 164 | } 165 | } 166 | ``` 167 | 168 | ### 4.使用查询 169 | ``` 170 | namespace App\Exports; 171 | 172 | use App\Invoice; 173 | use Mckue\Excel\Concerns\FromQuery; 174 | use Mckue\Excel\Concerns\Exportable; 175 | 176 | class InvoicesExport implements FromQuery 177 | { 178 | use Exportable; 179 | 180 | public function query() 181 | { 182 | return Invoice::query(); 183 | } 184 | } 185 | ``` 186 | ### 5.使用迭代器 187 | ``` 188 | namespace App\Exports; 189 | 190 | use App\Invoice; 191 | use Mckue\Excel\Concerns\FromIterator; 192 | use Mckue\Excel\Concerns\Exportable; 193 | 194 | class InvoicesExport implements FromIterator 195 | { 196 | use Exportable; 197 | 198 | public function iterator(): Iterator 199 | { 200 | ... 201 | } 202 | } 203 | ``` 204 | 在前面的示例中,我们使用Excel::download Facades来启动导出。 205 | ``` 206 | namespace App\Exports; 207 | 208 | use App\Invoice; 209 | use Mckue\Excel\Concerns\FromCollection; 210 | use Mckue\Excel\Concerns\Exportable; 211 | 212 | class InvoicesExport implements FromCollection 213 | { 214 | use Exportable; 215 | 216 | public function collection() 217 | { 218 | return Invoice::all(); 219 | } 220 | } 221 | ``` 222 | 我们现在可以下载导出而无需Facades: 223 | 224 | ``` 225 | return (new InvoicesExport)->download('invoices.xlsx'); 226 | ``` 227 | 或者将其存储在磁盘上: 228 | ``` 229 | return (new InvoicesExport)->store('invoices.xlsx', 's3'); 230 | ``` 231 | 232 | [更多文档可参考WIKI](https://github.com/carlin-rj/laravel-excel/wiki) 233 | 234 | 在此感谢 `xlswriter`的开发者`viest` 以及 `SpartnerNL/Laravel-Excel`的开发者。 235 | 如有什么问题可以及时反馈到github哦。 236 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mckue/laravel-excel", 3 | "license": "MIT", 4 | "description": "Laravel-Excel is based on Spartner NL/Lavel Excel code and switches to an xlswriter extension. If your project is using SparnerNL/Lavel Excel and there are big data export performance issues, and you don't want to modify a lot of code, then the current package may be very suitable for you.", 5 | "keywords": [ 6 | "laravel", 7 | "php", 8 | "xlswriter", 9 | "php-ext-xlswriter", 10 | "phpexcel", 11 | "excel", 12 | "csv", 13 | "export", 14 | "import", 15 | "batch" 16 | ], 17 | "autoload": { 18 | "psr-4": { 19 | "Mckue\\Excel\\": "src/" 20 | } 21 | }, 22 | "autoload-dev": { 23 | "psr-4": { 24 | "Mckue\\Excel\\Tests\\": "tests/" 25 | } 26 | }, 27 | "authors": [ 28 | { 29 | "name": "wanghaojie", 30 | "email": "814425737@qq.com" 31 | } 32 | ], 33 | "require": { 34 | "ext-xlswriter": "*", 35 | "ext-json": "*", 36 | "php": ">=8.1", 37 | "illuminate/support": "^8.0|^9.0|^10.0|^11.0", 38 | "composer/semver": "^3.3", 39 | "phpoffice/phpspreadsheet": "^1.18" 40 | }, 41 | "require-dev": { 42 | "orchestra/testbench": "^6.0|^7.0|^8.0", 43 | "phpunit/phpunit": "^10.3", 44 | "laravel/pint": "^1.13" 45 | }, 46 | "extra": { 47 | "laravel": { 48 | "providers": [ 49 | "Mckue\\Excel\\ExcelServiceProvider" 50 | ] 51 | }, 52 | "aliases": { 53 | "MckueExcel": "Mckue\\Excel\\Facades\\Excel" 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /config/mckue-excel.php: -------------------------------------------------------------------------------- 1 | [ 7 | /** 8 | * 1.固定内存模式(速度慢) 2.普通模式(内存占用大,速度快) 9 | */ 10 | 'model' => Excel::MODE_MEMORY, 11 | /* 12 | |-------------------------------------------------------------------------- 13 | | Chunk size 14 | |-------------------------------------------------------------------------- 15 | | 16 | | When using FromQuery, the query is automatically chunked. 17 | | Here you can specify how big the chunk should be. 18 | | 19 | */ 20 | 'chunk_size' => 1000, 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | Worksheet properties 25 | |-------------------------------------------------------------------------- 26 | | 27 | */ 28 | 'properties' => [ 29 | 30 | ], 31 | ], 32 | 33 | //'imports' => [ 34 | // 35 | // 36 | // 37 | //], 38 | 39 | /* 40 | |-------------------------------------------------------------------------- 41 | | Extension detector 42 | |-------------------------------------------------------------------------- 43 | | 44 | | Configure here which writer/reader type should be used when the package 45 | | needs to guess the correct type based on the extension alone. 46 | | 47 | */ 48 | 'extension_detector' => [ 49 | 'xlsx' => Excel::XLSX, 50 | 'xlsm' => Excel::XLSX, 51 | 'xltx' => Excel::XLSX, 52 | 'xltm' => Excel::XLSX, 53 | 'xls' => Excel::XLS, 54 | 'xlt' => Excel::XLS, 55 | 56 | /* 57 | |-------------------------------------------------------------------------- 58 | | PDF Extension 59 | |-------------------------------------------------------------------------- 60 | | 61 | | Configure here which Pdf driver should be used by default. 62 | | Available options: Excel::MPDF | Excel::TCPDF | Excel::DOMPDF 63 | | 64 | */ 65 | ], 66 | 67 | /* 68 | |-------------------------------------------------------------------------- 69 | | Transaction Handler 70 | |-------------------------------------------------------------------------- 71 | | 72 | | By default the import is wrapped in a transaction. This is useful 73 | | for when an import may fail and you want to retry it. With the 74 | | transactions, the previous import gets rolled-back. 75 | | 76 | | You can disable the transaction handler by setting this to null. 77 | | Or you can choose a custom made transaction handler here. 78 | | 79 | | Supported handlers: null|db 80 | | 81 | */ 82 | 'transactions' => [ 83 | 'handler' => 'null', 84 | 'db' => [ 85 | 'connection' => null, 86 | ], 87 | ], 88 | 89 | 'temporary_files' => [ 90 | 91 | /* 92 | |-------------------------------------------------------------------------- 93 | | Local Temporary Path 94 | |-------------------------------------------------------------------------- 95 | | 96 | | When exporting and importing files, we use a temporary file, before 97 | | storing reading or downloading. Here you can customize that path. 98 | | 99 | */ 100 | 'local_path' => storage_path('framework/cache/laravel-excel'), 101 | 102 | /* 103 | |-------------------------------------------------------------------------- 104 | | Remote Temporary Disk 105 | |-------------------------------------------------------------------------- 106 | | 107 | | When dealing with a multi server setup with queues in which you 108 | | cannot rely on having a shared local temporary path, you might 109 | | want to store the temporary file on a shared disk. During the 110 | | queue executing, we'll retrieve the temporary file from that 111 | | location instead. When left to null, it will always use 112 | | the local path. This setting only has effect when using 113 | | in conjunction with queued imports and exports. 114 | | 115 | */ 116 | 'remote_disk' => null, 117 | 'remote_prefix' => null, 118 | 119 | /* 120 | |-------------------------------------------------------------------------- 121 | | Force Resync 122 | |-------------------------------------------------------------------------- 123 | | 124 | | When dealing with a multi server setup as above, it's possible 125 | | for the clean up that occurs after entire queue has been run to only 126 | | cleanup the server that the last AfterImportJob runs on. The rest of the server 127 | | would still have the local temporary file stored on it. In this case your 128 | | local storage limits can be exceeded and future imports won't be processed. 129 | | To mitigate this you can set this config value to be true, so that after every 130 | | queued chunk is processed the local temporary file is deleted on the server that 131 | | processed it. 132 | | 133 | */ 134 | 'force_resync_remote' => null, 135 | ], 136 | ]; 137 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./src 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ./tests/ 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "laravel", 3 | "rules": { 4 | "binary_operator_spaces": { 5 | "default": "single_space", 6 | "operators": { 7 | "=>": "align_single_space_minimal" 8 | } 9 | }, 10 | "concat_space" : { 11 | "spacing": "one" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /pre-commit.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Get staging files 3 | STAGED_PHP_FILES=`git diff --cached --name-only --diff-filter=ACM "*.php" | grep -v "^vendor/"` 4 | 5 | # Check if PHP files were staged 6 | if [ ! -z "$STAGED_PHP_FILES" ]; then 7 | echo "# Running php-cs-fixer start" 8 | vendor/bin/pint --dirty 9 | echo "# Running php-cs-fixer end" 10 | fi 11 | -------------------------------------------------------------------------------- /src/Commands/InfoCommand.php: -------------------------------------------------------------------------------- 1 | extension_loaded('xlswriter') ? 'yes' : 'no', 40 | 'xlsWriter author' => function_exists('xlswriter_get_author') ? xlswriter_get_author() : '', 41 | ]; 42 | $execInfo = shell_exec('php --ri xlswriter'); 43 | $execInfo = explode(PHP_EOL, $execInfo); 44 | foreach ($execInfo as $index => $item) { 45 | if (empty($item) or strpos($item, '=>') == false) { 46 | unset($execInfo[$index]); 47 | } 48 | } 49 | foreach ($execInfo as $value) { 50 | $arr = explode('=>', $value); 51 | $result[trim($arr[0])] = trim($arr[1]); 52 | } 53 | 54 | $data = [ 55 | 'version' => $this->version, 56 | 'author' => 'mckue', 57 | 'docs' => $this->docsUrl 58 | ]; 59 | $this->displayTables('laravel-excel info:', $data); 60 | $this->displayTables('XlsWriter extension status:', $result); 61 | } 62 | 63 | 64 | protected function displayTables(string $title, $data): void 65 | { 66 | $this->line($title); 67 | $this->table([], $this->parseTable($data)); 68 | } 69 | 70 | /** 71 | * Make up the table for console display. 72 | * 73 | * @param $input 74 | * 75 | * @return array 76 | */ 77 | protected function parseTable(array $input): array 78 | { 79 | return array_map(static function ($key, $value) { 80 | return [ 81 | 'key' => $key, 82 | 'value' => $value, 83 | ]; 84 | }, array_keys($input), $input); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Concerns/Exportable.php: -------------------------------------------------------------------------------- 1 | headers ?? []; 17 | $fileName = $fileName ?? $this->fileName ?? null; 18 | $writerType = $writerType ?? $this->writerType ?? null; 19 | 20 | if (null === $fileName) { 21 | throw new NoFilenameGivenException(); 22 | } 23 | 24 | return $this->getExporter()->download($this, $fileName, $writerType, $headers); 25 | } 26 | 27 | /** 28 | * @param string|null $filePath 29 | * @param string|null $disk 30 | * @param string|null $writerType 31 | * @param mixed|array $diskOptions 32 | * @return bool 33 | * 34 | */ 35 | public function store(string $filePath = null, string $disk = null, string $writerType = null, array $diskOptions = []): bool 36 | { 37 | $filePath = $filePath ?? $this->filePath ?? null; 38 | 39 | if (null === $filePath) { 40 | throw NoFilePathGivenException::export(); 41 | } 42 | 43 | return $this->getExporter()->store( 44 | $this, 45 | $filePath, 46 | $disk ?? $this->disk ?? null, 47 | $writerType ?? $this->writerType ?? null, 48 | $diskOptions ?: $this->diskOptions ?? [] 49 | ); 50 | } 51 | 52 | /** 53 | * @param $writerType 54 | * @return string 55 | */ 56 | public function raw($writerType = null): string 57 | { 58 | $writerType = $writerType ?? $this->writerType ?? null; 59 | 60 | return $this->getExporter()->raw($this, $writerType); 61 | } 62 | 63 | /** 64 | * Create an HTTP response that represents the object. 65 | * 66 | * @param Request $request 67 | * @return Response 68 | * 69 | * @throws \Maatwebsite\Excel\Exceptions\NoFilenameGivenException 70 | */ 71 | public function toResponse(Request $request): Response 72 | { 73 | return $this->download(); 74 | } 75 | 76 | /** 77 | * @return Exporter 78 | */ 79 | private function getExporter(): Exporter 80 | { 81 | return app(Exporter::class); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Concerns/FromArray.php: -------------------------------------------------------------------------------- 1 | getFilePath($filePath); 32 | 33 | return $this->getImporter()->import( 34 | $this, 35 | $filePath, 36 | $disk ?? $this->disk ?? null, 37 | $readerType ?? $this->readerType ?? null 38 | ); 39 | } 40 | 41 | /** 42 | * @param string|UploadedFile|null $filePath 43 | * @param string|null $disk 44 | * @param string|null $readerType 45 | * @return array 46 | * 47 | * @throws NoFilePathGivenException 48 | */ 49 | public function toArray(string|UploadedFile|null $filePath = null, string $disk = null, string $readerType = null): array 50 | { 51 | $filePath = $this->getFilePath($filePath); 52 | 53 | return $this->getImporter()->toArray( 54 | $this, 55 | $filePath, 56 | $disk ?? $this->disk ?? null, 57 | $readerType ?? $this->readerType ?? null 58 | ); 59 | } 60 | 61 | /** 62 | * @param string|UploadedFile|null $filePath 63 | * @param string|null $disk 64 | * @param string|null $readerType 65 | * @return Collection 66 | * 67 | * @throws NoFilePathGivenException 68 | */ 69 | public function toCollection(string|UploadedFile|null $filePath = null, string $disk = null, string $readerType = null): Collection 70 | { 71 | $filePath = $this->getFilePath($filePath); 72 | 73 | return $this->getImporter()->toCollection( 74 | $this, 75 | $filePath, 76 | $disk ?? $this->disk ?? null, 77 | $readerType ?? $this->readerType ?? null 78 | ); 79 | } 80 | 81 | /** 82 | * @param OutputStyle $output 83 | * @return $this 84 | */ 85 | public function withOutput(OutputStyle $output): self 86 | { 87 | $this->output = $output; 88 | 89 | return $this; 90 | } 91 | 92 | /** 93 | * @return OutputStyle 94 | */ 95 | public function getConsoleOutput(): OutputStyle 96 | { 97 | if (!$this->output instanceof OutputStyle) { 98 | $this->output = new OutputStyle(new StringInput(''), new NullOutput()); 99 | } 100 | 101 | return $this->output; 102 | } 103 | 104 | /** 105 | * @param UploadedFile|string|null $filePath 106 | * @return UploadedFile|string 107 | * 108 | * @throws NoFilePathGivenException 109 | */ 110 | private function getFilePath(string|UploadedFile|null $filePath = null): string|UploadedFile 111 | { 112 | $filePath = $filePath ?? $this->filePath ?? null; 113 | 114 | if (null === $filePath) { 115 | throw NoFilePathGivenException::import(); 116 | } 117 | 118 | return $filePath; 119 | } 120 | 121 | /** 122 | * @return Importer 123 | */ 124 | private function getImporter(): Importer 125 | { 126 | return app(Importer::class); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Concerns/OnEachRow.php: -------------------------------------------------------------------------------- 1 | errors[] = $e; 21 | } 22 | 23 | /** 24 | * @return Throwable[]|Collection 25 | */ 26 | public function errors(): Collection 27 | { 28 | return new Collection($this->errors); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Concerns/SkipsFailures.php: -------------------------------------------------------------------------------- 1 | failures = array_merge($this->failures, $failures); 21 | } 22 | 23 | /** 24 | * @return Failure[]|Collection 25 | */ 26 | public function failures(): Collection 27 | { 28 | return new Collection($this->failures); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Concerns/SkipsOnError.php: -------------------------------------------------------------------------------- 1 | getDelegate(), $method)) { 23 | return call_user_func_array([$this->getDelegate(), $method], $parameters); 24 | } 25 | 26 | array_unshift($parameters, $this); 27 | 28 | return $this->__callMacro($method, $parameters); 29 | } 30 | 31 | /** 32 | * @return object 33 | */ 34 | abstract public function getDelegate(); 35 | } 36 | -------------------------------------------------------------------------------- /src/Events/AfterImport.php: -------------------------------------------------------------------------------- 1 | reader = $reader; 26 | $this->importable = $importable; 27 | } 28 | 29 | /** 30 | * @return Reader 31 | */ 32 | public function getReader(): Reader 33 | { 34 | return $this->reader; 35 | } 36 | 37 | /** 38 | * @return object 39 | */ 40 | public function getConcernable() 41 | { 42 | return $this->importable; 43 | } 44 | 45 | /** 46 | * @return mixed 47 | */ 48 | public function getDelegate() 49 | { 50 | return $this->reader; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Events/AfterSheet.php: -------------------------------------------------------------------------------- 1 | sheet = $sheet; 26 | $this->exportable = $exportable; 27 | } 28 | 29 | /** 30 | * @return Sheet 31 | */ 32 | public function getSheet(): Sheet 33 | { 34 | return $this->sheet; 35 | } 36 | 37 | /** 38 | * @return object 39 | */ 40 | public function getConcernable() 41 | { 42 | return $this->exportable; 43 | } 44 | 45 | /** 46 | * @return mixed 47 | */ 48 | public function getDelegate() 49 | { 50 | return $this->sheet; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Events/BeforeExport.php: -------------------------------------------------------------------------------- 1 | writer = $writer; 26 | $this->exportable = $exportable; 27 | } 28 | 29 | /** 30 | * @return Writer 31 | */ 32 | public function getWriter(): Writer 33 | { 34 | return $this->writer; 35 | } 36 | 37 | /** 38 | * @return object 39 | */ 40 | public function getConcernable(): object 41 | { 42 | return $this->exportable; 43 | } 44 | 45 | /** 46 | * @return mixed 47 | */ 48 | public function getDelegate():Writer 49 | { 50 | return $this->writer; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Events/BeforeImport.php: -------------------------------------------------------------------------------- 1 | reader = $reader; 26 | $this->importable = $importable; 27 | } 28 | 29 | /** 30 | * @return Reader 31 | */ 32 | public function getReader(): Reader 33 | { 34 | return $this->reader; 35 | } 36 | 37 | /** 38 | * @return object 39 | */ 40 | public function getConcernable() 41 | { 42 | return $this->importable; 43 | } 44 | 45 | /** 46 | * @return mixed 47 | */ 48 | public function getDelegate() 49 | { 50 | return $this->reader; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Events/BeforeSheet.php: -------------------------------------------------------------------------------- 1 | sheet = $sheet; 26 | $this->exportable = $exportable; 27 | } 28 | 29 | /** 30 | * @return Sheet 31 | */ 32 | public function getSheet(): Sheet 33 | { 34 | return $this->sheet; 35 | } 36 | 37 | /** 38 | * @return object 39 | */ 40 | public function getConcernable(): object 41 | { 42 | return $this->exportable; 43 | } 44 | 45 | /** 46 | * @return mixed 47 | */ 48 | public function getDelegate(): Sheet 49 | { 50 | return $this->sheet; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Events/BeforeWriting.php: -------------------------------------------------------------------------------- 1 | writer = $writer; 26 | $this->exportable = $exportable; 27 | } 28 | 29 | /** 30 | * @return Writer 31 | */ 32 | public function getWriter(): Writer 33 | { 34 | return $this->writer; 35 | } 36 | 37 | /** 38 | * @return object 39 | */ 40 | public function getConcernable(): object 41 | { 42 | return $this->exportable; 43 | } 44 | 45 | /** 46 | * @return mixed 47 | */ 48 | public function getDelegate(): Writer 49 | { 50 | return $this->writer; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Events/Event.php: -------------------------------------------------------------------------------- 1 | getConcernable() instanceof $concern; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Events/ImportFailed.php: -------------------------------------------------------------------------------- 1 | e = $e; 20 | } 21 | 22 | /** 23 | * @return Throwable 24 | */ 25 | public function getException(): Throwable 26 | { 27 | return $this->e; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Excel.php: -------------------------------------------------------------------------------- 1 | writer = $writer; 44 | $this->reader = $reader; 45 | $this->filesystem = $filesystem; 46 | } 47 | 48 | public function store(object $export, string $filePath, string $disk = null, string $writerType = null, array $diskOptions = []): bool 49 | { 50 | $temporaryFile = $this->export($export, $filePath, $writerType); 51 | 52 | $exported = $this->filesystem->disk($disk, $diskOptions)->copy( 53 | $temporaryFile, 54 | $filePath 55 | ); 56 | 57 | $temporaryFile->delete(); 58 | 59 | return $exported; 60 | } 61 | 62 | public function import(object $import, string|UploadedFile $filePath, string $disk = null, string $readerType = null): Reader 63 | { 64 | $readerType = FileTypeDetector::detect($filePath, $readerType); 65 | return $this->reader->read($import, $filePath, $readerType, $disk); 66 | } 67 | 68 | public function toArray(object $import, string|UploadedFile $filePath, string $disk = null, string $readerType = null): array 69 | { 70 | $readerType = FileTypeDetector::detect($filePath, $readerType); 71 | 72 | return $this->reader->toArray($import, $filePath, $readerType, $disk); 73 | } 74 | 75 | public function toCollection(object $import, string|UploadedFile $filePath, string $disk = null, string $readerType = null): Collection 76 | { 77 | $readerType = FileTypeDetector::detect($filePath, $readerType); 78 | 79 | return $this->reader->toCollection($import, $filePath, $readerType, $disk); 80 | } 81 | 82 | protected function export($export, string $fileName, string $writerType = null): TemporaryFile 83 | { 84 | $writerType = FileTypeDetector::detectStrict($fileName, $writerType); 85 | 86 | return $this->writer->export($export, $writerType); 87 | } 88 | 89 | public function download($export, string $fileName, string $writerType = null, array $headers = []): BinaryFileResponse 90 | { 91 | return response()->download( 92 | $this->export($export, $fileName, $writerType)->getLocalPath(), 93 | $fileName, 94 | $headers 95 | )->deleteFileAfterSend(); 96 | } 97 | 98 | public function raw(object $export, string $writerType): string 99 | { 100 | $temporaryFile = $this->writer->export($export, $writerType); 101 | 102 | $contents = $temporaryFile->contents(); 103 | $temporaryFile->delete(); 104 | 105 | return $contents; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/ExcelServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 27 | if ($this->app instanceof LumenApplication) { 28 | $this->app->configure('mckue-excel'); 29 | } else { 30 | $this->publishes([ 31 | $this->getConfigFile() => config_path('mckue-excel.php'), 32 | ], 'config'); 33 | } 34 | } 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | public function register(): void 41 | { 42 | $this->mergeConfigFrom( 43 | $this->getConfigFile(), 44 | 'mckue-excel' 45 | ); 46 | 47 | $this->app->singleton(TransactionManager::class, function ($app) { 48 | return new TransactionManager($app); 49 | }); 50 | 51 | $this->app->bind(TransactionHandler::class, function ($app) { 52 | return $app->make(TransactionManager::class)->driver(); 53 | }); 54 | 55 | $this->app->bind(TemporaryFileFactory::class, function () { 56 | return new TemporaryFileFactory( 57 | config('mckue-excel.temporary_files.local_path', config('mckue-excel.exports.temp_path', storage_path('framework/laravel-excel'))), 58 | config('mckue-excel.temporary_files.remote_disk') 59 | ); 60 | }); 61 | 62 | $this->app->bind(Filesystem::class, function ($app) { 63 | return new Filesystem($app->make('filesystem')); 64 | }); 65 | 66 | $this->app->bind('mckue.excel', function ($app) { 67 | return new Excel( 68 | $app->make(Writer::class), 69 | $app->make(Reader::class), 70 | $app->make(Filesystem::class) 71 | ); 72 | }); 73 | 74 | $this->app->alias('mckue.excel', Excel::class); 75 | $this->app->alias('mckue.excel', Exporter::class); 76 | $this->app->alias('mckue.excel', Importer::class); 77 | 78 | Collection::mixin(new DownloadCollection); 79 | Collection::mixin(new StoreCollection); 80 | 81 | $this->registerCommands(); 82 | } 83 | 84 | /** 85 | * @return string 86 | */ 87 | protected function getConfigFile(): string 88 | { 89 | return __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'mckue-excel.php'; 90 | } 91 | 92 | 93 | /** 94 | * register the commands 95 | */ 96 | private function registerCommands(): void 97 | { 98 | $this->commands($this->commands); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Exceptions/ConcernConflictException.php: -------------------------------------------------------------------------------- 1 | failures = $failures; 22 | 23 | parent::__construct(); 24 | } 25 | 26 | /** 27 | * @return Failure[]|Collection 28 | */ 29 | public function failures(): Collection 30 | { 31 | return new Collection($this->failures); 32 | } 33 | 34 | /** 35 | * @return int[] 36 | */ 37 | public function skippedRows(): array 38 | { 39 | return $this->failures()->map->row()->all(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Exceptions/SheetNotFoundException.php: -------------------------------------------------------------------------------- 1 | disk = $disk; 38 | $this->name = $name; 39 | $this->diskOptions = $diskOptions; 40 | } 41 | 42 | /** 43 | * @param string $name 44 | * @param array $arguments 45 | * @return mixed 46 | */ 47 | public function __call($name, $arguments) 48 | { 49 | return $this->disk->{$name}(...$arguments); 50 | } 51 | 52 | /** 53 | * @param string $destination 54 | * @param string|resource $contents 55 | * @return bool 56 | */ 57 | public function put(string $destination, $contents): bool 58 | { 59 | return $this->disk->put($destination, $contents, $this->diskOptions); 60 | } 61 | 62 | /** 63 | * @param TemporaryFile $source 64 | * @param string $destination 65 | * @return bool 66 | */ 67 | public function copy(TemporaryFile $source, string $destination): bool 68 | { 69 | $readStream = $source->readStream(); 70 | 71 | if (realpath($destination)) { 72 | $tempStream = fopen($destination, 'rb+'); 73 | $success = stream_copy_to_stream($readStream, $tempStream) !== false; 74 | 75 | if (is_resource($tempStream)) { 76 | fclose($tempStream); 77 | } 78 | } else { 79 | $success = $this->put($destination, $readStream); 80 | } 81 | 82 | if (is_resource($readStream)) { 83 | fclose($readStream); 84 | } 85 | 86 | return $success; 87 | } 88 | 89 | /** 90 | * @param string $filename 91 | */ 92 | public function touch(string $filename) 93 | { 94 | $this->disk->put($filename, '', $this->diskOptions); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Files/Filesystem.php: -------------------------------------------------------------------------------- 1 | filesystem = $filesystem; 20 | } 21 | 22 | /** 23 | * @param string|null $disk 24 | * @param array $diskOptions 25 | * @return Disk 26 | */ 27 | public function disk(string $disk = null, array $diskOptions = []): Disk 28 | { 29 | return new Disk( 30 | $this->filesystem->disk($disk), 31 | $disk, 32 | $diskOptions 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Files/LocalTemporaryFile.php: -------------------------------------------------------------------------------- 1 | filePath = realpath($filePath); 20 | } 21 | 22 | /** 23 | * @return string 24 | */ 25 | public function getLocalPath(): string 26 | { 27 | return $this->filePath; 28 | } 29 | 30 | public function getDirName(): string 31 | { 32 | return pathinfo($this->filePath)['dirname']; 33 | } 34 | 35 | public function getFileName(): string 36 | { 37 | return pathinfo($this->filePath)['basename']; 38 | } 39 | 40 | /** 41 | * @return bool 42 | */ 43 | public function exists(): bool 44 | { 45 | return file_exists($this->filePath); 46 | } 47 | 48 | /** 49 | * @return bool 50 | */ 51 | public function delete(): bool 52 | { 53 | if (@unlink($this->filePath) || !$this->exists()) { 54 | return true; 55 | } 56 | 57 | return unlink($this->filePath); 58 | } 59 | 60 | /** 61 | * @return resource 62 | */ 63 | public function readStream() 64 | { 65 | return fopen($this->getLocalPath(), 'rb+'); 66 | } 67 | 68 | /** 69 | * @return string 70 | */ 71 | public function contents(): string 72 | { 73 | return file_get_contents($this->filePath); 74 | } 75 | 76 | /** 77 | * @param @param string|resource $contents 78 | */ 79 | public function put($contents) 80 | { 81 | file_put_contents($this->filePath, $contents); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Files/RemoteTemporaryFile.php: -------------------------------------------------------------------------------- 1 | disk = $disk; 35 | $this->filename = $filename; 36 | $this->localTemporaryFile = $localTemporaryFile; 37 | 38 | $this->disk()->touch($filename); 39 | } 40 | 41 | public function __sleep() 42 | { 43 | return ['disk', 'filename', 'localTemporaryFile']; 44 | } 45 | 46 | /** 47 | * @return string 48 | */ 49 | public function getLocalPath(): string 50 | { 51 | return $this->localTemporaryFile->getLocalPath(); 52 | } 53 | 54 | public function getDirName(): string 55 | { 56 | return $this->localTemporaryFile->getDirName(); 57 | } 58 | 59 | public function getFileName(): string 60 | { 61 | return $this->localTemporaryFile->getFileName(); 62 | } 63 | 64 | /** 65 | * @return bool 66 | */ 67 | public function existsLocally(): bool 68 | { 69 | return $this->localTemporaryFile->exists(); 70 | } 71 | 72 | /** 73 | * @return bool 74 | */ 75 | public function exists(): bool 76 | { 77 | return $this->disk()->exists($this->filename); 78 | } 79 | 80 | /** 81 | * @return bool 82 | */ 83 | public function deleteLocalCopy(): bool 84 | { 85 | return $this->localTemporaryFile->delete(); 86 | } 87 | 88 | /** 89 | * @return bool 90 | */ 91 | public function delete(): bool 92 | { 93 | // we don't need to delete local copy as it's deleted at end of each chunk 94 | if (!config('mckue-excel.temporary_files.force_resync_remote')) { 95 | $this->deleteLocalCopy(); 96 | } 97 | 98 | return $this->disk()->delete($this->filename); 99 | } 100 | 101 | /** 102 | * @return TemporaryFile 103 | */ 104 | public function sync(): TemporaryFile 105 | { 106 | if (!$this->localTemporaryFile->exists()) { 107 | touch($this->localTemporaryFile->getLocalPath()); 108 | } 109 | 110 | $this->disk()->copy( 111 | $this, 112 | $this->localTemporaryFile->getLocalPath() 113 | ); 114 | 115 | return $this; 116 | } 117 | 118 | /** 119 | * Store on remote disk. 120 | */ 121 | public function updateRemote() 122 | { 123 | $this->disk()->copy( 124 | $this->localTemporaryFile, 125 | $this->filename 126 | ); 127 | } 128 | 129 | /** 130 | * @return resource 131 | */ 132 | public function readStream() 133 | { 134 | return $this->disk()->readStream($this->filename); 135 | } 136 | 137 | /** 138 | * @return string 139 | */ 140 | public function contents(): string 141 | { 142 | return $this->disk()->get($this->filename); 143 | } 144 | 145 | /** 146 | * @param string|resource $contents 147 | */ 148 | public function put($contents) 149 | { 150 | $this->disk()->put($this->filename, $contents); 151 | } 152 | 153 | /** 154 | * @return Disk 155 | */ 156 | public function disk(): Disk 157 | { 158 | return $this->diskInstance ?: $this->diskInstance = app(Filesystem::class)->disk($this->disk); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/Files/TemporaryFile.php: -------------------------------------------------------------------------------- 1 | getRealPath(), 'rb'); 56 | } elseif ($disk === null && realpath($filePath) !== false) { 57 | $readStream = fopen($filePath, 'rb'); 58 | } else { 59 | $readStream = app('filesystem')->disk($disk)->readStream($filePath); 60 | } 61 | 62 | $this->put($readStream); 63 | 64 | if (is_resource($readStream)) { 65 | fclose($readStream); 66 | } 67 | 68 | return $this->sync(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Files/TemporaryFileFactory.php: -------------------------------------------------------------------------------- 1 | temporaryPath = $temporaryPath; 26 | $this->temporaryDisk = $temporaryDisk; 27 | } 28 | 29 | /** 30 | * @param string|null $fileExtension 31 | * @return TemporaryFile 32 | */ 33 | public function make(string $fileExtension = null): TemporaryFile 34 | { 35 | if (null !== $this->temporaryDisk) { 36 | return $this->makeRemote($fileExtension); 37 | } 38 | 39 | return $this->makeLocal(null, $fileExtension); 40 | } 41 | 42 | /** 43 | * @param string|null $fileName 44 | * @param string|null $fileExtension 45 | * @return LocalTemporaryFile 46 | */ 47 | public function makeLocal(string $fileName = null, string $fileExtension = null): LocalTemporaryFile 48 | { 49 | if (!file_exists($this->temporaryPath) && !mkdir($concurrentDirectory = $this->temporaryPath) && !is_dir($concurrentDirectory)) { 50 | throw new \RuntimeException(sprintf('Directory "%s" was not created', $concurrentDirectory)); 51 | } 52 | 53 | return new LocalTemporaryFile( 54 | $this->temporaryPath . DIRECTORY_SEPARATOR . ($fileName ?: $this->generateFilename($fileExtension)) 55 | ); 56 | } 57 | 58 | /** 59 | * @param string|null $fileExtension 60 | * @return RemoteTemporaryFile 61 | */ 62 | private function makeRemote(string $fileExtension = null): RemoteTemporaryFile 63 | { 64 | $filename = $this->generateFilename($fileExtension); 65 | 66 | return new RemoteTemporaryFile( 67 | $this->temporaryDisk, 68 | config('mckue-excel.temporary_files.remote_prefix') . $filename, 69 | $this->makeLocal($filename) 70 | ); 71 | } 72 | 73 | /** 74 | * @param string|null $fileExtension 75 | * @return string 76 | */ 77 | private function generateFilename(string $fileExtension = null): string 78 | { 79 | return 'laravel-excel-' . Str::random(32) . ($fileExtension ? '.' . $fileExtension : ''); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/HasEventBus.php: -------------------------------------------------------------------------------- 1 | $listener) { 25 | $this->events[$event][] = $listener; 26 | } 27 | } 28 | 29 | public function clearListeners(): void 30 | { 31 | $this->events = []; 32 | } 33 | 34 | /** 35 | * Register a global event listener. 36 | * 37 | * @param string $event 38 | * @param callable $listener 39 | */ 40 | public static function listen(string $event, callable $listener): void 41 | { 42 | static::$globalEvents[$event][] = $listener; 43 | } 44 | 45 | /** 46 | * @param object $event 47 | */ 48 | public function raise($event): void 49 | { 50 | foreach ($this->listeners($event) as $listener) { 51 | $listener($event); 52 | } 53 | } 54 | 55 | /** 56 | * @param object $event 57 | * @return callable[] 58 | */ 59 | public function listeners(object $event): array 60 | { 61 | $name = \get_class($event); 62 | 63 | $localListeners = $this->events[$name] ?? []; 64 | $globalListeners = static::$globalEvents[$name] ?? []; 65 | 66 | return array_merge($globalListeners, $localListeners); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Helpers/ArrayHelper.php: -------------------------------------------------------------------------------- 1 | getClientOriginalExtension(); 28 | } 29 | 30 | if (trim($extension) === '') { 31 | throw new NoTypeDetectedException(); 32 | } 33 | 34 | return config('mckue-excel.extension_detector.' . strtolower($extension)); 35 | } 36 | 37 | /** 38 | * @param string $filePath 39 | * @param string|null $type 40 | * @return string 41 | * 42 | * @throws NoTypeDetectedException 43 | */ 44 | public static function detectStrict(string $filePath, string $type = null): string 45 | { 46 | $type = static::detect($filePath, $type); 47 | 48 | if (!$type) { 49 | throw new NoTypeDetectedException(); 50 | } 51 | 52 | return $type; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Importer.php: -------------------------------------------------------------------------------- 1 | headingRow() 23 | : self::DEFAULT_HEADING_ROW; 24 | } 25 | 26 | /** 27 | * @param WithHeadingRow|mixed $importable 28 | * @return int 29 | */ 30 | public static function determineStartRow($importable): int 31 | { 32 | if ($importable instanceof WithStartRow) { 33 | return $importable->startRow(); 34 | } 35 | 36 | // The start row is the row after the heading row if we have one! 37 | return $importable instanceof WithHeadingRow 38 | ? self::headingRow($importable) + 1 39 | : self::DEFAULT_HEADING_ROW; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Imports/ModelImporter.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 22 | } 23 | 24 | /** 25 | * @param Excel $excel 26 | * @param ToModel $import 27 | * @param int $startRow 28 | * @throws ValidationException 29 | */ 30 | public function import(Excel $excel, ToModel $import, int $startRow = 1): void 31 | { 32 | $batchSize = $import instanceof WithBatchInserts ? $import->batchSize() : 1; 33 | $withMapping = $import instanceof WithMapping; 34 | $withValidation = $import instanceof WithValidation && method_exists($import, 'prepareForValidation'); 35 | $endColumn = $import instanceof WithColumnLimit ? $import->endColumn() : null; 36 | 37 | $this->manager->setRemembersRowNumber(method_exists($import, 'rememberRowNumber')); 38 | 39 | $worksheet = $excel->setSkipRows($startRow); 40 | $endColumnIndex = $endColumn ? Excel::columnIndexFromString($endColumn) : null; 41 | 42 | $i = $startRow - 1; 43 | while (($rowArray = $worksheet->nextRow()) !== NULL) { 44 | $i++; 45 | 46 | if ($endColumnIndex !== null) { 47 | $rowArray = array_slice((array)$rowArray, 0, $endColumnIndex + 1); 48 | } 49 | 50 | //如果是空数组则直接跳过 51 | if ($import instanceof SkipsEmptyRows && ArrayHelper::isEmpty($rowArray)) { 52 | continue; 53 | } 54 | 55 | if ($withValidation) { 56 | $rowArray = $import->prepareForValidation($rowArray, $i); 57 | } 58 | 59 | if ($withMapping) { 60 | $rowArray = $import->map($rowArray); 61 | } 62 | 63 | $this->manager->add( 64 | $i, 65 | $rowArray 66 | ); 67 | 68 | // Flush each batch. 69 | if (($i % $batchSize) === 0) { 70 | $this->manager->flush($import, $batchSize > 1); 71 | $i = 0; 72 | } 73 | } 74 | 75 | $this->manager->flush($import, $batchSize > 1); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Imports/ModelManager.php: -------------------------------------------------------------------------------- 1 | validator = $validator; 39 | } 40 | 41 | /** 42 | * @param int $row 43 | * @param array $attributes 44 | */ 45 | public function add(int $row, array $attributes) 46 | { 47 | $this->rows[$row] = $attributes; 48 | } 49 | 50 | /** 51 | * @param bool $remembersRowNumber 52 | */ 53 | public function setRemembersRowNumber(bool $remembersRowNumber) 54 | { 55 | $this->remembersRowNumber = $remembersRowNumber; 56 | } 57 | 58 | /** 59 | * @param ToModel $import 60 | * @param bool $massInsert 61 | * 62 | * @throws ValidationException 63 | */ 64 | public function flush(ToModel $import, bool $massInsert = false) 65 | { 66 | if ($import instanceof WithValidation) { 67 | $this->validateRows($import); 68 | } 69 | 70 | if ($massInsert) { 71 | $this->massFlush($import); 72 | } else { 73 | $this->singleFlush($import); 74 | } 75 | 76 | $this->rows = []; 77 | } 78 | 79 | /** 80 | * @param ToModel $import 81 | * @param array $attributes 82 | * @param int|null $rowNumber 83 | * @return Model[]|Collection 84 | */ 85 | public function toModels(ToModel $import, array $attributes, $rowNumber = null): Collection 86 | { 87 | if ($this->remembersRowNumber) { 88 | $import->rememberRowNumber($rowNumber); 89 | } 90 | 91 | $model = $import->model($attributes); 92 | 93 | if (null !== $model) { 94 | return \is_array($model) ? new Collection($model) : new Collection([$model]); 95 | } 96 | 97 | return new Collection([]); 98 | } 99 | 100 | /** 101 | * @param ToModel $import 102 | */ 103 | private function massFlush(ToModel $import) 104 | { 105 | $this->rows() 106 | ->flatMap(function (array $attributes, $index) use ($import) { 107 | return $this->toModels($import, $attributes, $index); 108 | }) 109 | ->mapToGroups(function ($model) { 110 | return [\get_class($model) => $this->prepare($model)->getAttributes()]; 111 | }) 112 | ->each(function (Collection $models, string $model) use ($import) { 113 | try { 114 | /* @var Model $model */ 115 | 116 | if ($import instanceof WithUpserts) { 117 | $model::query()->upsert( 118 | $models->toArray(), 119 | $import->uniqueBy(), 120 | $import instanceof WithUpsertColumns ? $import->upsertColumns() : null 121 | ); 122 | } else { 123 | $model::query()->insert($models->toArray()); 124 | } 125 | } catch (Throwable $e) { 126 | if ($import instanceof SkipsOnError) { 127 | $import->onError($e); 128 | } else { 129 | throw $e; 130 | } 131 | } 132 | }); 133 | } 134 | 135 | /** 136 | * @param ToModel $import 137 | */ 138 | private function singleFlush(ToModel $import) 139 | { 140 | $this 141 | ->rows() 142 | ->each(function (array $attributes, $index) use ($import) { 143 | $this->toModels($import, $attributes, $index)->each(function (Model $model) use ($import) { 144 | try { 145 | if ($import instanceof WithUpserts) { 146 | $model->upsert( 147 | $model->getAttributes(), 148 | $import->uniqueBy(), 149 | $import instanceof WithUpsertColumns ? $import->upsertColumns() : null 150 | ); 151 | } else { 152 | $model->saveOrFail(); 153 | } 154 | } catch (Throwable $e) { 155 | if ($import instanceof SkipsOnError) { 156 | $import->onError($e); 157 | } else { 158 | throw $e; 159 | } 160 | } 161 | }); 162 | }); 163 | } 164 | 165 | /** 166 | * @param Model $model 167 | * @return Model 168 | */ 169 | private function prepare(Model $model): Model 170 | { 171 | if ($model->usesTimestamps()) { 172 | $time = $model->freshTimestamp(); 173 | 174 | $updatedAtColumn = $model->getUpdatedAtColumn(); 175 | 176 | // If model has updated at column and not manually provided. 177 | if ($updatedAtColumn && null === $model->{$updatedAtColumn}) { 178 | $model->setUpdatedAt($time); 179 | } 180 | 181 | $createdAtColumn = $model->getCreatedAtColumn(); 182 | 183 | // If model has created at column and not manually provided. 184 | if ($createdAtColumn && null === $model->{$createdAtColumn}) { 185 | $model->setCreatedAt($time); 186 | } 187 | } 188 | 189 | return $model; 190 | } 191 | 192 | /** 193 | * @param WithValidation $import 194 | * 195 | * @throws ValidationException 196 | */ 197 | private function validateRows(WithValidation $import) 198 | { 199 | try { 200 | $this->validator->validate($this->rows, $import); 201 | } catch (RowSkippedException $e) { 202 | foreach ($e->skippedRows() as $row) { 203 | unset($this->rows[$row]); 204 | } 205 | } 206 | } 207 | 208 | /** 209 | * @return Collection 210 | */ 211 | private function rows(): Collection 212 | { 213 | return new Collection($this->rows); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/MappedReader.php: -------------------------------------------------------------------------------- 1 | mapping(); 23 | 24 | $rowIndex = 0; 25 | while ($row = $worksheet->nextRow()) { 26 | foreach ($mapping as $name => $coordinate) { 27 | [$column, $targetRow] = $this->coordinateToPosition($coordinate); // Convert A1-like coordinate to numeric form 28 | 29 | if ($rowIndex === $targetRow && isset($row[$column])) { 30 | $mapped[$name] = $row[$column]; 31 | } 32 | } 33 | $rowIndex++; 34 | } 35 | 36 | if ($import instanceof ToModel) { 37 | $model = $import->model($mapped); 38 | 39 | if ($model) { 40 | $model->saveOrFail(); 41 | } 42 | } 43 | 44 | if ($import instanceof ToCollection) { 45 | $import->collection(new Collection($mapped)); 46 | } 47 | 48 | if ($import instanceof ToArray) { 49 | $import->array($mapped); 50 | } 51 | } 52 | 53 | private function coordinateToPosition($coordinate): array 54 | { 55 | $column = Excel::columnIndexFromString(substr($coordinate, 0, 1)) - 1; // Convert column letter to index (0-based) 56 | $row = (int)substr($coordinate, 1) - 1; // Rows in xlswriter are 0-indexed 57 | 58 | return [$column, $row]; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Mixins/DownloadCollection.php: -------------------------------------------------------------------------------- 1 | collection = $collection->toBase(); 41 | $this->withHeadings = $withHeading; 42 | } 43 | 44 | /** 45 | * @return Collection 46 | */ 47 | public function collection() 48 | { 49 | return $this->collection; 50 | } 51 | 52 | /** 53 | * @return array 54 | */ 55 | public function headings(): array 56 | { 57 | if (!$this->withHeadings) { 58 | return []; 59 | } 60 | 61 | $firstRow = $this->collection->first(); 62 | 63 | if ($firstRow instanceof Arrayable || \is_object($firstRow)) { 64 | return array_keys(Sheet::mapArraybleRow($firstRow)); 65 | } 66 | 67 | return $this->collection->collapse()->keys()->all(); 68 | } 69 | }; 70 | 71 | return $export->download($fileName, $writerType, $responseHeaders); 72 | }; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Mixins/StoreCollection.php: -------------------------------------------------------------------------------- 1 | collection = $collection->toBase(); 39 | $this->withHeadings = $withHeadings; 40 | } 41 | 42 | /** 43 | * @return Collection 44 | */ 45 | public function collection() 46 | { 47 | return $this->collection; 48 | } 49 | 50 | /** 51 | * @return array 52 | */ 53 | public function headings(): array 54 | { 55 | if (!$this->withHeadings) { 56 | return []; 57 | } 58 | 59 | return is_array($first = $this->collection->first()) 60 | ? $this->collection->collapse()->keys()->all() 61 | : array_keys($first->toArray()); 62 | } 63 | }; 64 | 65 | return $export->store($filePath, $disk, $writerType); 66 | }; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Reader.php: -------------------------------------------------------------------------------- 1 | transaction = $transaction; 56 | $this->temporaryFileFactory = $temporaryFileFactory; 57 | } 58 | 59 | public function __sleep() 60 | { 61 | return ['reader', 'sheetImports', 'currentFile', 'temporaryFileFactory', 'reader']; 62 | } 63 | 64 | public function __wakeup() 65 | { 66 | $this->transaction = app(TransactionHandler::class); 67 | } 68 | 69 | /** 70 | * @param object $import 71 | * @param string|UploadedFile $filePath 72 | * @param string|null $readerType 73 | * @param string|null $disk 74 | * @return $this 75 | * @throws SheetNotFoundException 76 | * @throws Throwable 77 | */ 78 | public function read(object $import, string|UploadedFile $filePath, string $readerType = null, string $disk = null):self 79 | { 80 | $this->reader = $this->getReader($import, $filePath, $disk); 81 | 82 | try { 83 | $this->loadSpreadsheet($import); 84 | 85 | ($this->transaction)(function () use ($import) { 86 | foreach ($this->sheetImports as $index => $sheetImport) { 87 | if ($sheet = $this->getSheet($import, $sheetImport, $index)) { 88 | $sheet->import($sheetImport, $sheet->getStartRow($sheetImport)); 89 | } 90 | } 91 | }); 92 | 93 | $this->afterImport($import); 94 | } catch (Throwable $e) { 95 | $this->raise(new ImportFailed($e)); 96 | $this->garbageCollect(); 97 | throw $e; 98 | } 99 | 100 | return $this; 101 | } 102 | 103 | /** 104 | * @param object $import 105 | * @param string|UploadedFile $filePath 106 | * @param string|null $readerType 107 | * @param string|null $disk 108 | * @return array 109 | * @throws SheetNotFoundException 110 | */ 111 | public function toArray(object $import, string|UploadedFile $filePath, string $readerType = null, string $disk = null): array 112 | { 113 | $this->reader = $this->getReader($import, $filePath, $readerType, $disk); 114 | 115 | $this->loadSpreadsheet($import); 116 | 117 | $sheets = []; 118 | foreach ($this->sheetImports as $index => $sheetImport) { 119 | if ($sheet = $this->getSheet($import, $sheetImport, $index)) { 120 | $sheets[$index] = $sheet->toArray($sheetImport, $sheet->getStartRow($sheetImport)); 121 | } 122 | } 123 | 124 | $this->afterImport($import); 125 | 126 | return $sheets; 127 | } 128 | 129 | /** 130 | * @param object $import 131 | * @param string|UploadedFile $filePath 132 | * @param string|null $readerType 133 | * @param string|null $disk 134 | * @return Collection 135 | * @throws SheetNotFoundException 136 | */ 137 | public function toCollection(object $import, string|UploadedFile $filePath, string $readerType = null, string $disk = null): Collection 138 | { 139 | $this->reader = $this->getReader($import, $filePath, $readerType, $disk); 140 | $this->loadSpreadsheet($import); 141 | 142 | $sheets = new Collection(); 143 | foreach ($this->sheetImports as $index => $sheetImport) { 144 | if ($sheet = $this->getSheet($import, $sheetImport, $index)) { 145 | $sheets->put($index, $sheet->toCollection($sheetImport, $sheet->getStartRow($sheetImport))); 146 | } 147 | } 148 | 149 | $this->afterImport($import); 150 | 151 | return $sheets; 152 | } 153 | 154 | /** 155 | * @return \Vtiful\Kernel\Excel 156 | */ 157 | public function getDelegate(): \Vtiful\Kernel\Excel 158 | { 159 | return $this->reader; 160 | } 161 | 162 | /** 163 | * @param object $import 164 | */ 165 | public function loadSpreadsheet(object $import): void 166 | { 167 | $this->sheetImports = $this->buildSheetImports($import); 168 | $this->beforeImport($import); 169 | } 170 | 171 | /** 172 | * @param object $import 173 | */ 174 | public function beforeImport(object $import): void 175 | { 176 | $this->raise(new BeforeImport($this, $import)); 177 | } 178 | 179 | /** 180 | * @param object $import 181 | */ 182 | public function afterImport(object $import): void 183 | { 184 | $this->raise(new AfterImport($this, $import)); 185 | 186 | $this->garbageCollect(); 187 | } 188 | 189 | /** 190 | * @param object $import 191 | * @return array 192 | */ 193 | public function getWorksheets(object $import): array 194 | { 195 | // Csv doesn't have worksheets. 196 | if (!method_exists($this->reader, 'listWorksheetNames')) { 197 | return ['Worksheet' => $import]; 198 | } 199 | 200 | $worksheets = []; 201 | $worksheetNames = $this->reader->sheetList(); 202 | if ($import instanceof WithMultipleSheets) { 203 | $sheetImports = $import->sheets(); 204 | 205 | foreach ($sheetImports as $index => $sheetImport) { 206 | // Translate index to name. 207 | if (is_numeric($index)) { 208 | $index = $worksheetNames[$index] ?? $index; 209 | } 210 | 211 | // Specify with worksheet name should have which import. 212 | $worksheets[$index] = $sheetImport; 213 | } 214 | 215 | // Load specific sheets. 216 | if (method_exists($this->reader, 'setLoadSheetsOnly')) { 217 | $this->reader->setLoadSheetsOnly( 218 | collect($worksheetNames)->intersect(array_keys($worksheets))->values()->all() 219 | ); 220 | } 221 | } else { 222 | // Each worksheet the same import class. 223 | foreach ($worksheetNames as $name) { 224 | $worksheets[$name] = $import; 225 | } 226 | } 227 | 228 | return $worksheets; 229 | } 230 | 231 | /** 232 | * @param object $import 233 | * @param object $sheetImport 234 | * @param string|int $sheetName 235 | * @return Sheet|null 236 | * 237 | * @throws SheetNotFoundException 238 | */ 239 | protected function getSheet(object $import, object $sheetImport, string|int $sheetName): Sheet|null 240 | { 241 | try { 242 | return Sheet::make($this->reader, $sheetName); 243 | } catch (SheetNotFoundException $e) { 244 | if ($import instanceof SkipsUnknownSheets) { 245 | $import->onUnknownSheet($sheetName); 246 | 247 | return null; 248 | } 249 | 250 | if ($sheetImport instanceof SkipsUnknownSheets) { 251 | $sheetImport->onUnknownSheet($sheetName); 252 | 253 | return null; 254 | } 255 | 256 | throw $e; 257 | } 258 | } 259 | 260 | /** 261 | * @param object $import 262 | * @return array 263 | */ 264 | private function buildSheetImports(object $import): array 265 | { 266 | $sheetImports = []; 267 | if ($import instanceof WithMultipleSheets) { 268 | $sheetImports = $import->sheets(); 269 | } else { 270 | $sheetImports[] = $import; 271 | } 272 | return $sheetImports; 273 | } 274 | 275 | /** 276 | * @param object $import 277 | * @param string|UploadedFile $filePath 278 | * @param string|null $readerType 279 | * @param string|null $disk 280 | * @return \Vtiful\Kernel\Excel 281 | * 282 | */ 283 | private function getReader(object $import, string|UploadedFile $filePath, string|null $readerType = null, string|null $disk = null): \Vtiful\Kernel\Excel 284 | { 285 | 286 | if ($import instanceof WithEvents) { 287 | $this->registerListeners($import->registerEvents()); 288 | } 289 | 290 | $fileExtension = pathinfo($filePath, PATHINFO_EXTENSION); 291 | $temporaryFile = $this->temporaryFileFactory->makeLocal(null, $fileExtension); 292 | $this->currentFile = $temporaryFile->copyFrom( 293 | $filePath, 294 | $disk 295 | ); 296 | return (new \Vtiful\Kernel\Excel(['path' => $this->currentFile->getDirName()]))->openFile($this->currentFile->getFileName()); 297 | } 298 | 299 | /** 300 | * Garbage collect. 301 | */ 302 | private function garbageCollect(): void 303 | { 304 | $this->clearListeners(); 305 | 306 | // Force garbage collecting 307 | unset($this->sheetImports, $this->reader); 308 | 309 | $this->currentFile->delete(); 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /src/Sheet.php: -------------------------------------------------------------------------------- 1 | worksheet = $worksheet; 63 | $this->chunkSize = config('mckue-excel.exports.chunk_size', 100); 64 | $this->temporaryFileFactory = app(TemporaryFileFactory::class); 65 | } 66 | 67 | /** 68 | * @throws SheetNotFoundException 69 | */ 70 | public static function make(Excel $spreadsheet, string|int $index): Sheet 71 | { 72 | if (is_numeric($index)) { 73 | return self::byIndex($spreadsheet, $index); 74 | } 75 | 76 | return self::byName($spreadsheet, $index); 77 | } 78 | 79 | /** 80 | * @throws SheetNotFoundException 81 | */ 82 | public static function byIndex(Excel $spreadsheet, int $index): Sheet 83 | { 84 | $sheetList = $spreadsheet->sheetList(); 85 | if (! isset($sheetList[$index])) { 86 | throw SheetNotFoundException::byIndex($index, count($sheetList)); 87 | } 88 | 89 | return new static($spreadsheet->openSheet($sheetList[$index])); 90 | } 91 | 92 | /** 93 | * @throws SheetNotFoundException 94 | */ 95 | public static function byName(Excel $spreadsheet, string $name): Sheet 96 | { 97 | $sheetList = $spreadsheet->sheetList(); 98 | $sheetList = array_flip($sheetList); 99 | if (! isset($sheetList[$name])) { 100 | throw SheetNotFoundException::byName($name); 101 | } 102 | 103 | return new static($spreadsheet->openSheet($name)); 104 | } 105 | 106 | public function open(object $sheetExport): void 107 | { 108 | $this->exportable = $sheetExport; 109 | 110 | if ($sheetExport instanceof WithEvents) { 111 | $this->registerListeners($sheetExport->registerEvents()); 112 | } 113 | 114 | $this->raise(new BeforeSheet($this, $this->exportable)); 115 | 116 | if (($sheetExport instanceof FromQuery || $sheetExport instanceof FromCollection || $sheetExport instanceof FromArray) && $sheetExport instanceof FromView) { 117 | throw ConcernConflictException::queryOrCollectionAndView(); 118 | } 119 | 120 | if ($sheetExport instanceof WithHeadings) { 121 | if (ArrayHelper::hasMultipleRows($sheetExport->headings())) { 122 | $this->worksheet->data($sheetExport->headings()); 123 | } else { 124 | $this->worksheet->header($sheetExport->headings()); 125 | } 126 | } 127 | 128 | //if ($sheetExport instanceof WithCharts) { 129 | // $this->addCharts($sheetExport->charts()); 130 | //} 131 | } 132 | 133 | public function export(object $sheetExport): void 134 | { 135 | $this->open($sheetExport); 136 | 137 | if ($sheetExport instanceof FromView) { 138 | $this->fromView($sheetExport); 139 | } else { 140 | if ($sheetExport instanceof FromQuery) { 141 | $this->fromQuery($sheetExport); 142 | } 143 | 144 | if ($sheetExport instanceof FromCollection) { 145 | $this->fromCollection($sheetExport); 146 | } 147 | 148 | if ($sheetExport instanceof FromArray) { 149 | $this->fromArray($sheetExport); 150 | } 151 | 152 | if ($sheetExport instanceof FromIterator) { 153 | $this->fromIterator($sheetExport); 154 | } 155 | 156 | if ($sheetExport instanceof FromGenerator) { 157 | $this->fromGenerator($sheetExport); 158 | } 159 | 160 | if ($sheetExport instanceof WithMergeCells) { 161 | $cells = $sheetExport->mergeCells(); 162 | foreach ($cells as $cell => $data) { 163 | $this->worksheet->mergeCells($cell, $data); 164 | } 165 | } 166 | } 167 | 168 | if ($sheetExport instanceof ShouldAutoSize) { 169 | $this->autoSize(); 170 | } 171 | 172 | $this->close($sheetExport); 173 | } 174 | 175 | public function autoSize(): void 176 | { 177 | foreach ($this->maxColumnWidths as $index => $width) { 178 | $columnLetter = Excel::stringFromColumnIndex($index); 179 | $this->worksheet->setColumn("$columnLetter:$columnLetter", $width + 3, $this->worksheet->getHandle()); 180 | } 181 | } 182 | 183 | public function import(object $import, int $startRow = 1): void 184 | { 185 | if ($import instanceof WithEvents) { 186 | $this->registerListeners($import->registerEvents()); 187 | } 188 | 189 | $this->raise(new BeforeSheet($this, $import)); 190 | 191 | if ($import instanceof WithMappedCells) { 192 | app(MappedReader::class)->map($import, $this->worksheet); 193 | } else { 194 | if ($import instanceof ToModel) { 195 | app(ModelImporter::class)->import($this->worksheet, $import, $startRow); 196 | } 197 | 198 | if ($import instanceof ToCollection) { 199 | $rows = $this->toCollection($import, $startRow); 200 | 201 | if ($import instanceof WithValidation) { 202 | $rows = $this->validated($import, $startRow, $rows); 203 | } 204 | 205 | $import->collection($rows); 206 | } 207 | 208 | if ($import instanceof ToArray) { 209 | $rows = $this->toArray($import, $startRow); 210 | 211 | if ($import instanceof WithValidation) { 212 | $rows = $this->validated($import, $startRow, $rows); 213 | } 214 | 215 | $import->array($rows); 216 | } 217 | 218 | if ($import instanceof OnEachRow) { 219 | $this->onEachRow($import, $startRow); 220 | } 221 | } 222 | 223 | $this->raise(new AfterSheet($this, $import)); 224 | } 225 | 226 | public function onEachRow(object $import, int $startRow = 1): void 227 | { 228 | $endColumn = $import instanceof WithColumnLimit ? $import->endColumn() : null; 229 | $worksheet = $this->worksheet->setSkipRows($startRow); 230 | $endColumnIndex = $endColumn ? Excel::columnIndexFromString($endColumn) : null; 231 | 232 | $i = $startRow - 1; 233 | while (($rowArray = $worksheet->nextRow()) !== null) { 234 | $i++; 235 | 236 | if ($endColumnIndex !== null) { 237 | $rowArray = array_slice((array) $rowArray, 0, $endColumnIndex + 1); 238 | } 239 | 240 | if ($import instanceof SkipsEmptyRows && ArrayHelper::isEmpty($rowArray)) { 241 | continue; 242 | } 243 | 244 | if ($import instanceof WithValidation) { 245 | 246 | if (method_exists($import, 'prepareForValidation')) { 247 | $rowArray = $import->prepareForValidation($rowArray, $i); 248 | } 249 | 250 | app(RowValidator::class)->validate($rowArray, $import); 251 | } else { 252 | $import->onRow($rowArray); 253 | } 254 | } 255 | } 256 | 257 | public function toArray(object $import, int $startRow = 1): array 258 | { 259 | $endColumn = $import instanceof WithColumnLimit ? $import->endColumn() : null; 260 | $rows = []; 261 | 262 | $worksheet = $this->worksheet->setSkipRows($startRow); 263 | 264 | $i = $startRow - 1; 265 | $endColumnIndex = $endColumn ? Excel::columnIndexFromString($endColumn) : null; 266 | while (($rowArray = $worksheet->nextRow()) !== null) { 267 | $i++; 268 | if ($endColumnIndex !== null) { 269 | $rowArray = array_slice((array) $rowArray, 0, $endColumnIndex + 1); 270 | } 271 | 272 | if ($import instanceof SkipsEmptyRows && ArrayHelper::isEmpty($rowArray)) { 273 | continue; 274 | } 275 | 276 | if ($import && method_exists($import, 'isEmptyWhen') && $import->isEmptyWhen($rowArray)) { 277 | continue; 278 | } 279 | 280 | if ($import instanceof WithMapping) { 281 | $rowArray = $import->map($rowArray); 282 | } 283 | 284 | if ($import instanceof WithValidation && method_exists($import, 'prepareForValidation')) { 285 | $rowArray = $import->prepareForValidation($rowArray, $i); 286 | } 287 | 288 | $rows[] = $rowArray; 289 | } 290 | 291 | return $rows; 292 | } 293 | 294 | public function toCollection(object $import, int $startRow = 1): Collection 295 | { 296 | $rows = $this->toArray($import, $startRow); 297 | 298 | return new Collection(array_map(static function (array $row) { 299 | return new Collection($row); 300 | }, $rows)); 301 | } 302 | 303 | public function close(object $sheetExport): void 304 | { 305 | //if ($sheetExport instanceof WithDrawings) { 306 | // $this->addDrawings($sheetExport->drawings()); 307 | //} 308 | 309 | $this->exportable = $sheetExport; 310 | 311 | if ($sheetExport instanceof WithColumnFormatting) { 312 | $sheetExport->formatColumns($this->worksheet); 313 | } 314 | 315 | if ($sheetExport instanceof WithRowFormatting) { 316 | $sheetExport->formatRows($this->worksheet); 317 | } 318 | 319 | if ($sheetExport instanceof WithColumnWidths) { 320 | foreach ($sheetExport->columnWidths() as $column => $width) { 321 | $this->setColumnWidth($column, $width); 322 | 323 | } 324 | } 325 | 326 | if ($sheetExport instanceof WithStyles) { 327 | $sheetExport->styles($this->worksheet); 328 | } 329 | 330 | $this->raise(new AfterSheet($this, $this->exportable)); 331 | 332 | $this->clearListeners(); 333 | } 334 | 335 | private function setColumnWidth(string $column, float|int $width): void 336 | { 337 | $this->worksheet->setColumn($column, $width, $this->worksheet->getHandle()); 338 | } 339 | 340 | public function fromView(FromView $sheetExport, $sheetIndex = null): void 341 | { 342 | $temporaryFile = $this->temporaryFileFactory->makeLocal(null, 'html'); 343 | $temporaryFile->put($sheetExport->view()->render()); 344 | 345 | $spreadsheet = new Spreadsheet(); 346 | 347 | /** @var Html $reader */ 348 | $reader = IOFactory::createReader('Html'); 349 | 350 | // If no sheetIndex given, insert content into the last sheet 351 | $sheetIndex = $sheetIndex ?? $spreadsheet->getSheetCount() - 1; 352 | $reader->setSheetIndex($sheetIndex); 353 | $reader->loadIntoExisting($temporaryFile->getLocalPath(), $spreadsheet); 354 | $temporaryFile->delete(); 355 | 356 | // Step 2: 保存这个Excel到临时文件 357 | $temporaryXlsxFile = $this->temporaryFileFactory->makeLocal(null, 'xlsx'); 358 | $writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); 359 | $writer->save($temporaryXlsxFile->getLocalPath()); 360 | 361 | $excel = new Excel(['path' => $temporaryXlsxFile->getDirName()]); 362 | $sheet = $excel->openFile($temporaryXlsxFile->getFileName()); 363 | $worksheet = Sheet::make($sheet, $sheetIndex); 364 | 365 | while (($rowArray = $worksheet->nextRow()) !== null) { 366 | $this->appendRows([$rowArray], $sheetExport); 367 | } 368 | 369 | $temporaryXlsxFile->delete(); // 删除临时文件 370 | } 371 | 372 | public function fromQuery(FromQuery $sheetExport): void 373 | { 374 | $sheetExport->query()->chunk($this->getChunkSize($sheetExport), function ($chunk) use ($sheetExport) { 375 | $this->appendRows($chunk, $sheetExport); 376 | }); 377 | } 378 | 379 | public function fromCollection(FromCollection $sheetExport): void 380 | { 381 | $this->appendRows($sheetExport->collection()->all(), $sheetExport); 382 | } 383 | 384 | public function fromArray(FromArray $sheetExport): void 385 | { 386 | $this->appendRows($sheetExport->array(), $sheetExport); 387 | } 388 | 389 | public function fromIterator(FromIterator $sheetExport): void 390 | { 391 | $this->appendRows($sheetExport->iterator(), $sheetExport); 392 | } 393 | 394 | public function fromGenerator(FromGenerator $sheetExport): void 395 | { 396 | $this->appendRows($sheetExport->generator(), $sheetExport); 397 | } 398 | 399 | public function chunkSize(int $chunkSize): self 400 | { 401 | $this->chunkSize = $chunkSize; 402 | 403 | return $this; 404 | } 405 | 406 | public function getDelegate(): Excel 407 | { 408 | return $this->worksheet; 409 | } 410 | 411 | ///** 412 | // * @param Chart|Chart[] $charts 413 | // */ 414 | //public function addCharts($charts) 415 | //{ 416 | // $charts = \is_array($charts) ? $charts : [$charts]; 417 | // 418 | // foreach ($charts as $chart) { 419 | // $this->worksheet->addChart($chart); 420 | // } 421 | //} 422 | public function appendRows(iterable $rows, object $sheetExport): void 423 | { 424 | if (method_exists($sheetExport, 'prepareRows')) { 425 | $rows = $sheetExport->prepareRows($rows); 426 | } 427 | 428 | $rows = (new Collection($rows))->flatMap(function ($row) use ($sheetExport) { 429 | if ($sheetExport instanceof WithMapping) { 430 | $row = $sheetExport->map($row); 431 | } 432 | 433 | $row = static::mapArraybleRow($row); 434 | 435 | $this->calculateMaxColumnWidths($row); 436 | 437 | return ArrayHelper::ensureMultipleRows($row); 438 | })->toArray(); 439 | 440 | $this->worksheet->data($rows); 441 | 442 | } 443 | 444 | protected function calculateMaxColumnWidths(array $row): void 445 | { 446 | foreach ($row as $index => $value) { 447 | $length = mb_strlen((string) $value); 448 | if (!isset($this->maxColumnWidths[$index]) || $length > $this->maxColumnWidths[$index]) { 449 | $this->maxColumnWidths[$index] = $length; 450 | } 451 | } 452 | } 453 | 454 | public static function mapArraybleRow(mixed $row): array 455 | { 456 | // When dealing with eloquent models, we'll skip the relations 457 | // as we won't be able to display them anyway. 458 | if (is_object($row) && method_exists($row, 'attributesToArray')) { 459 | return $row->attributesToArray(); 460 | } 461 | 462 | if ($row instanceof Arrayable) { 463 | return $row->toArray(); 464 | } 465 | 466 | // Convert StdObjects to arrays 467 | if (is_object($row)) { 468 | return json_decode(json_encode($row), true); 469 | } 470 | 471 | return $row; 472 | } 473 | 474 | public function getStartRow($sheetImport): int 475 | { 476 | return HeadingRowExtractor::determineStartRow($sheetImport); 477 | } 478 | 479 | protected function validated(WithValidation $import, int $startRow, $rows): Collection|array 480 | { 481 | $toValidate = (new Collection($rows))->mapWithKeys(function ($row, $index) use ($startRow) { 482 | return [($startRow + $index) => $row]; 483 | }); 484 | 485 | try { 486 | app(RowValidator::class)->validate($toValidate->toArray(), $import); 487 | } catch (RowSkippedException $e) { 488 | foreach ($e->skippedRows() as $row) { 489 | unset($rows[$row - $startRow]); 490 | } 491 | } 492 | 493 | return $rows; 494 | } 495 | 496 | private function getChunkSize(object $export): int 497 | { 498 | if ($export instanceof WithCustomChunkSize) { 499 | return $export->chunkSize(); 500 | } 501 | 502 | return $this->chunkSize; 503 | } 504 | } 505 | -------------------------------------------------------------------------------- /src/Transactions/DbTransactionHandler.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 20 | } 21 | 22 | /** 23 | * @param callable $callback 24 | * @return mixed 25 | * 26 | * @throws \Throwable 27 | */ 28 | public function __invoke(callable $callback) 29 | { 30 | return $this->connection->transaction($callback); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Transactions/NullTransactionHandler.php: -------------------------------------------------------------------------------- 1 | row = $row; 39 | $this->attribute = $attribute; 40 | $this->errors = $errors; 41 | $this->values = $values; 42 | } 43 | 44 | /** 45 | * @return int 46 | */ 47 | public function row(): int 48 | { 49 | return $this->row; 50 | } 51 | 52 | /** 53 | * @return string 54 | */ 55 | public function attribute(): string 56 | { 57 | return $this->attribute; 58 | } 59 | 60 | /** 61 | * @return array 62 | */ 63 | public function errors(): array 64 | { 65 | return $this->errors; 66 | } 67 | 68 | /** 69 | * @return array 70 | */ 71 | public function values(): array 72 | { 73 | return $this->values; 74 | } 75 | 76 | /** 77 | * @return array 78 | */ 79 | public function toArray() 80 | { 81 | return collect($this->errors)->map(function ($message) { 82 | return __('There was an error on row :row. :message', ['row' => $this->row, 'message' => $message]); 83 | })->all(); 84 | } 85 | 86 | /** 87 | * @return array 88 | */ 89 | #[\ReturnTypeWillChange] 90 | public function jsonSerialize() 91 | { 92 | return [ 93 | 'row' => $this->row(), 94 | 'attribute' => $this->attribute(), 95 | 'errors' => $this->errors(), 96 | 'values' => $this->values(), 97 | ]; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Validators/RowValidator.php: -------------------------------------------------------------------------------- 1 | validator = $validator; 25 | } 26 | 27 | /** 28 | * @param array $rows 29 | * @param WithValidation $import 30 | * 31 | * @throws ValidationException 32 | * @throws RowSkippedException 33 | */ 34 | public function validate(array $rows, WithValidation $import) 35 | { 36 | $rules = $this->rules($import); 37 | $messages = $this->messages($import); 38 | $attributes = $this->attributes($import); 39 | 40 | try { 41 | $validator = $this->validator->make($rows, $rules, $messages, $attributes); 42 | 43 | if (method_exists($import, 'withValidator')) { 44 | $import->withValidator($validator); 45 | } 46 | 47 | $validator->validate(); 48 | } catch (IlluminateValidationException $e) { 49 | $failures = []; 50 | foreach ($e->errors() as $attribute => $messages) { 51 | $row = strtok($attribute, '.'); 52 | $attributeName = strtok(''); 53 | $attributeName = $attributes['*.' . $attributeName] ?? $attributeName; 54 | 55 | $failures[] = new Failure( 56 | $row, 57 | $attributeName, 58 | str_replace($attribute, $attributeName, $messages), 59 | $rows[$row] ?? [] 60 | ); 61 | } 62 | 63 | if ($import instanceof SkipsOnFailure) { 64 | $import->onFailure(...$failures); 65 | throw new RowSkippedException(...$failures); 66 | } 67 | 68 | throw new ValidationException( 69 | $e, 70 | $failures 71 | ); 72 | } 73 | } 74 | 75 | /** 76 | * @param WithValidation $import 77 | * @return array 78 | */ 79 | private function messages(WithValidation $import): array 80 | { 81 | return method_exists($import, 'customValidationMessages') 82 | ? $this->formatKey($import->customValidationMessages()) 83 | : []; 84 | } 85 | 86 | /** 87 | * @param WithValidation $import 88 | * @return array 89 | */ 90 | private function attributes(WithValidation $import): array 91 | { 92 | return method_exists($import, 'customValidationAttributes') 93 | ? $this->formatKey($import->customValidationAttributes()) 94 | : []; 95 | } 96 | 97 | /** 98 | * @param WithValidation $import 99 | * @return array 100 | */ 101 | private function rules(WithValidation $import): array 102 | { 103 | return $this->formatKey($import->rules()); 104 | } 105 | 106 | /** 107 | * @param array $elements 108 | * @return array 109 | */ 110 | private function formatKey(array $elements): array 111 | { 112 | return collect($elements)->mapWithKeys(function ($rule, $attribute) { 113 | $attribute = Str::startsWith($attribute, '*.') ? $attribute : '*.' . $attribute; 114 | 115 | return [$attribute => $this->formatRule($rule)]; 116 | })->all(); 117 | } 118 | 119 | /** 120 | * @param string|object|callable|array $rules 121 | * @return string|array 122 | */ 123 | private function formatRule($rules) 124 | { 125 | if (is_array($rules)) { 126 | foreach ($rules as $rule) { 127 | $formatted[] = $this->formatRule($rule); 128 | } 129 | 130 | return $formatted ?? []; 131 | } 132 | 133 | if (is_object($rules) || is_callable($rules)) { 134 | return $rules; 135 | } 136 | 137 | if (Str::contains($rules, 'required_') && preg_match('/(.*):(.*),(.*)/', $rules, $matches)) { 138 | $column = Str::startsWith($matches[2], '*.') ? $matches[2] : '*.' . $matches[2]; 139 | 140 | return $matches[1] . ':' . $column . ',' . $matches[3]; 141 | } 142 | 143 | return $rules; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Validators/ValidationException.php: -------------------------------------------------------------------------------- 1 | validator, $previous->response, $previous->errorBag); 21 | $this->failures = $failures; 22 | } 23 | 24 | /** 25 | * @return string[] 26 | */ 27 | public function errors(): array 28 | { 29 | return collect($this->failures)->map->toArray()->all(); 30 | } 31 | 32 | /** 33 | * @return array 34 | */ 35 | public function failures(): array 36 | { 37 | return $this->failures; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Writer.php: -------------------------------------------------------------------------------- 1 | temporaryFileFactory = $temporaryFileFactory; 40 | } 41 | 42 | /** 43 | * @param object $export 44 | * @param string $writerType 45 | * @return TemporaryFile 46 | * @throws SheetNotFoundException 47 | */ 48 | public function export(object $export, string $writerType): TemporaryFile 49 | { 50 | $temporaryFile = $this->temporaryFileFactory->makeLocal(null, strtolower($writerType)); 51 | 52 | $this->open($export, $temporaryFile); 53 | 54 | $sheetExports = [$export]; 55 | if ($export instanceof WithMultipleSheets) { 56 | $sheetExports = $export->sheets(); 57 | } 58 | 59 | foreach ($sheetExports as $k => $sheetExport) { 60 | if ($sheetExport instanceof WithTitle) { 61 | $sheetName = $sheetExport->title(); 62 | } else { 63 | $sheetName = 'Sheet' . ($k+1); 64 | } 65 | $this->addNewSheet($sheetName)->export($sheetExport); 66 | } 67 | 68 | return $this->write($export, $temporaryFile, $writerType); 69 | } 70 | 71 | /** 72 | * @param object $export 73 | * @param TemporaryFile $temporaryFile 74 | * @return $this 75 | * @throws SheetNotFoundException 76 | */ 77 | public function open(object $export, TemporaryFile $temporaryFile): self 78 | { 79 | $this->exportable = $export; 80 | 81 | if ($export instanceof WithEvents) { 82 | $this->registerListeners($export->registerEvents()); 83 | } 84 | 85 | $this->spreadsheet = new \Vtiful\Kernel\Excel(['path'=>$temporaryFile->getDirName()]); 86 | 87 | //多sheet必须给表头名字 88 | $sheetName = $export instanceof WithTitle ? $export->title() : 'Sheet1'; 89 | if ($export instanceof WithMultipleSheets) { 90 | $sheetExports = $export->sheets(); 91 | $sheetExport = reset($sheetExports); 92 | if (! $sheetExport instanceof WithTitle) { 93 | throw SheetNotFoundException::byTitle(); 94 | } 95 | $sheetName = $sheetExport->title(); 96 | } 97 | 98 | if (config('mckue-excel.exports.model') === Excel::MODE_NORMAL) { 99 | $this->spreadsheet->fileName($temporaryFile->getFileName(), $sheetName); 100 | } else { 101 | //wps打不开需要给false 102 | $this->spreadsheet->constMemory($temporaryFile->getFileName(), $sheetName, false); 103 | } 104 | 105 | $this->handleDocumentProperties($export); 106 | 107 | //if ($export instanceof WithBackgroundColor) { 108 | // $defaultStyle = $this->spreadsheet->getDefaultStyle(); 109 | // $backgroundColor = $export->backgroundColor(); 110 | // 111 | // if (is_string($backgroundColor)) { 112 | // $defaultStyle->getFill()->setFillType(Fill::FILL_SOLID)->getStartColor()->setRGB($backgroundColor); 113 | // } 114 | // 115 | // if (is_array($backgroundColor)) { 116 | // $defaultStyle->applyFromArray(['fill' => $backgroundColor]); 117 | // } 118 | // 119 | // if ($backgroundColor instanceof Color) { 120 | // $defaultStyle->getFill()->setFillType(Fill::FILL_SOLID)->setStartColor($backgroundColor); 121 | // } 122 | //} 123 | 124 | //if ($export instanceof WithDefaultStyles) { 125 | // $defaultStyle = $this->spreadsheet->getDefaultStyle(); 126 | // $styles = $export->defaultStyles($defaultStyle); 127 | // 128 | // if (is_array($styles)) { 129 | // $defaultStyle->applyFromArray($styles); 130 | // } 131 | //} 132 | 133 | $this->raise(new BeforeExport($this, $this->exportable)); 134 | 135 | return $this; 136 | } 137 | 138 | /** 139 | * @param object $export 140 | * @param TemporaryFile $temporaryFile 141 | * @param string $writerType 142 | * @return TemporaryFile 143 | */ 144 | public function write(object $export, TemporaryFile $temporaryFile, string $writerType): TemporaryFile 145 | { 146 | $this->exportable = $export; 147 | 148 | //$this->spreadsheet->setActiveSheetIndex(0); 149 | 150 | $this->raise(new BeforeWriting($this, $this->exportable)); 151 | 152 | $this->spreadsheet->output(); 153 | 154 | if ($temporaryFile instanceof RemoteTemporaryFile) { 155 | $temporaryFile->updateRemote(); 156 | $temporaryFile->deleteLocalCopy(); 157 | } 158 | 159 | $this->clearListeners(); 160 | 161 | $this->spreadsheet->close(); 162 | 163 | unset($this->spreadsheet); 164 | 165 | return $temporaryFile; 166 | } 167 | 168 | /** 169 | * @param string $sheetName 170 | * @return Sheet 171 | */ 172 | public function addNewSheet(string $sheetName):Sheet 173 | { 174 | if (! $this->spreadsheet->existSheet($sheetName)) { 175 | $this->spreadsheet->addSheet($sheetName); 176 | } 177 | return new Sheet($this->spreadsheet); 178 | } 179 | 180 | /** 181 | * @return \Vtiful\Kernel\Excel 182 | */ 183 | public function getDelegate(): \Vtiful\Kernel\Excel 184 | { 185 | return $this->spreadsheet; 186 | } 187 | 188 | 189 | /** 190 | * @param int $sheetIndex 191 | * @return Sheet 192 | * 193 | */ 194 | public function getSheetByIndex(int $sheetIndex): Sheet 195 | { 196 | return new Sheet($this->getDelegate()->getSheet($sheetIndex)); 197 | } 198 | 199 | /** 200 | * @param string $concern 201 | * @return bool 202 | */ 203 | public function hasConcern(string $concern): bool 204 | { 205 | return $this->exportable instanceof $concern; 206 | } 207 | 208 | /** 209 | * @param object $export 210 | */ 211 | protected function handleDocumentProperties(object $export): void 212 | { 213 | $properties = config('mckue-excel.exports.properties', []); 214 | 215 | if ($export instanceof WithProperties) { 216 | $properties = array_merge($properties, $export->properties()); 217 | } 218 | 219 | $props = $this->spreadsheet; 220 | 221 | foreach (array_filter($properties) as $property => $value) { 222 | switch ($property) { 223 | case 'gridline': 224 | $props->gridline($value); 225 | break; 226 | case 'zoom': 227 | $props->zoom($value); 228 | break; 229 | } 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /tests/ExcelTest.php: -------------------------------------------------------------------------------- 1 | __DIR__, // xlsx文件保存路径 11 | ]; 12 | $excel = new \Vtiful\Kernel\Excel($config); 13 | 14 | // fileName 会自动创建一个工作表,你可以自定义该工作表名称,工作表名称为可选参数 15 | $filePath = $excel->fileName('tutorial01.xlsx', 'sheet1') 16 | ->header(['Item', 'Cost']) 17 | ->data([['Item1', 'Cost2']]) 18 | ->data([ 19 | ['Rent', 1000], 20 | ['Gas', 100], 21 | ['Food', 300], 22 | ['Gym', 50], 23 | ]) 24 | ->output(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/ExportTest.php: -------------------------------------------------------------------------------- 1 | SUT = $this->app->make(Excel::class); 23 | } 24 | 25 | public function testStoreFromArray() 26 | { 27 | $export = new class implements FromArray, WithHeadings, WithTitle 28 | { 29 | use Exportable; 30 | 31 | public function title(): string 32 | { 33 | return 'Sheet1'; 34 | } 35 | 36 | public function array(): array 37 | { 38 | return [[ 39 | 1, 2, 3, 40 | ], [ 41 | 3, 4, 5, 42 | ], [ 43 | 6, 7, 8, 44 | ]]; 45 | } 46 | 47 | public function headings(): array 48 | { 49 | return [ 50 | ['test1', 'test2', 'test3'], 51 | ['test111', 'test222', 'test333'], 52 | ]; 53 | } 54 | }; 55 | 56 | \Mckue\Excel\Facades\Excel::store($export, 'test/test.xlsx'); 57 | //echo $this->SUT->store(new TestExport(), 'test/test.xlsx', null, 'xlsx'); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/ImportTest.php: -------------------------------------------------------------------------------- 1 | excel = $this->app->make(Excel::class); 17 | } 18 | 19 | public function testStoreFromArray() 20 | { 21 | $export = new class implements ToArray 22 | { 23 | public function array(array $array): void 24 | { 25 | dump($array); 26 | } 27 | }; 28 | 29 | \Mckue\Excel\Facades\Excel::import($export, 'test/test.xlsx'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | set('filesystems.disks.local.root', __DIR__ . '/Data/Disks/Local'); 26 | $app['config']->set('filesystems.disks.test', [ 27 | 'driver' => 'local', 28 | 'root' => __DIR__ . '/Data/Disks/Test', 29 | ]); 30 | 31 | $app['config']->set('database.default', 'testing'); 32 | $app['config']->set('database.connections.testing', [ 33 | 'driver' => 'mysql', 34 | 'host' => env('DB_HOST'), 35 | 'port' => env('DB_PORT'), 36 | 'database' => env('DB_DATABASE'), 37 | 'username' => env('DB_USERNAME'), 38 | 'password' => env('DB_PASSWORD'), 39 | ]); 40 | 41 | $app['config']->set('view.paths', [ 42 | __DIR__ . '/Data/Stubs/Views', 43 | ]); 44 | } 45 | 46 | } 47 | --------------------------------------------------------------------------------