├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── Column.php ├── DataColumn.php ├── SerialColumn.php └── Spreadsheet.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Yii 2 Spreadsheet Data Export extension Change Log 2 | ================================================== 3 | 4 | 1.0.5, September 12, 2019 5 | ------------------------- 6 | 7 | - Enh #23: Added `Spreadsheet::$writerCreator` allowing setup of custom spreadsheet writer (klimov-paul) 8 | 9 | 10 | 1.0.4, July 10, 2019 11 | -------------------- 12 | 13 | - Enh #21: Added `Spreadsheet::$startRowIndex` allowing to skip lines at the sheet document beginning (klimov-paul) 14 | 15 | 16 | 1.0.3, January 24, 2019 17 | ----------------------- 18 | 19 | - Bug #11: Fixed `Spreadsheet::send()` fails for 'Xlsx' writer (klimov-paul) 20 | 21 | 22 | 1.0.2, April 18, 2018 23 | --------------------- 24 | 25 | - Bug #4: Fixed `Spreadsheet::save()` forces directory name to lowercase (klimov-paul) 26 | - Bug #5: Fixed `Spreadsheet::createDataColumn()` sets column format to 'text' instead of 'raw' (klimov-paul) 27 | 28 | 29 | 1.0.1, April 9, 2018 30 | -------------------- 31 | 32 | - Enh #2: `Spreadsheet::send()` now throws `\RuntimeException` in case temporary file can not be created (Eseperio, klimov-paul) 33 | 34 | 35 | 1.0.0, February 13, 2018 36 | ------------------------ 37 | 38 | - Initial release. 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The Yii framework is free software. It is released under the terms of 2 | the following BSD License. 3 | 4 | Copyright © 2015 by Yii2tech (https://github.com/yii2tech) 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions 9 | are met: 10 | 11 | * Redistributions of source code must retain the above copyright 12 | notice, this list of conditions and the following disclaimer. 13 | * Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in 15 | the documentation and/or other materials provided with the 16 | distribution. 17 | * Neither the name of Yii2tech nor the names of its 18 | contributors may be used to endorse or promote products derived 19 | from this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 24 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 25 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 26 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 27 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 30 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 31 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 32 | POSSIBILITY OF SUCH DAMAGE. 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

Spreadsheet Data Export extension for Yii2

6 |
7 |

8 | 9 | This extension provides ability to export data to spreadsheet, e.g. Excel, LibreOffice etc. 10 | 11 | For license information check the [LICENSE](LICENSE.md)-file. 12 | 13 | [![Latest Stable Version](https://img.shields.io/packagist/v/yii2tech/spreadsheet.svg)](https://packagist.org/packages/yii2tech/spreadsheet) 14 | [![Total Downloads](https://img.shields.io/packagist/dt/yii2tech/spreadsheet.svg)](https://packagist.org/packages/yii2tech/spreadsheet) 15 | [![Build Status](https://travis-ci.org/yii2tech/spreadsheet.svg?branch=master)](https://travis-ci.org/yii2tech/spreadsheet) 16 | 17 | 18 | Installation 19 | ------------ 20 | 21 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 22 | 23 | Either run 24 | 25 | ``` 26 | php composer.phar require --prefer-dist yii2tech/spreadsheet 27 | ``` 28 | 29 | or add 30 | 31 | ```json 32 | "yii2tech/spreadsheet": "*" 33 | ``` 34 | 35 | to the require section of your composer.json. 36 | 37 | 38 | Usage 39 | ----- 40 | 41 | This extension provides ability to export data to a spreadsheet, e.g. Excel, LibreOffice etc. 42 | It is powered by [phpoffice/phpspreadsheet](https://github.com/PHPOffice/PhpSpreadsheet) library. 43 | Export is performed via `\yii2tech\spreadsheet\Spreadsheet` instance, which provides interface similar to `\yii\grid\GridView` widget. 44 | 45 | Example: 46 | 47 | ```php 48 | new ArrayDataProvider([ 55 | 'allModels' => [ 56 | [ 57 | 'name' => 'some name', 58 | 'price' => '9879', 59 | ], 60 | [ 61 | 'name' => 'name 2', 62 | 'price' => '79', 63 | ], 64 | ], 65 | ]), 66 | 'columns' => [ 67 | [ 68 | 'attribute' => 'name', 69 | 'contentOptions' => [ 70 | 'alignment' => [ 71 | 'horizontal' => 'center', 72 | 'vertical' => 'center', 73 | ], 74 | ], 75 | ], 76 | [ 77 | 'attribute' => 'price', 78 | ], 79 | ], 80 | ]); 81 | $exporter->save('/path/to/file.xls'); 82 | ``` 83 | 84 | Please, refer to `\yii2tech\spreadsheet\Column` class for the information about column properties and configuration specifications. 85 | 86 | While running web application you can use `\yii2tech\spreadsheet\Spreadsheet::send()` method to send a result file to 87 | the browser through download dialog: 88 | 89 | ```php 90 | new ActiveDataProvider([ 102 | 'query' => Item::find(), 103 | ]), 104 | ]); 105 | return $exporter->send('items.xls'); 106 | } 107 | } 108 | ``` 109 | 110 | 111 | ### Multiple sheet files 112 | 113 | You can create an output file with multiple worksheets (tabs). For example: you may want to export data about 114 | equipment used in the office, keeping monitors, mouses, keyboards and so on in separated listings but in the same file. 115 | To do so you will need to manually call `\yii2tech\spreadsheet\Spreadsheet::render()` method with different configuration 116 | before creating final file. For example: 117 | 118 | ```php 119 | 'Monitors', 127 | 'dataProvider' => new ActiveDataProvider([ 128 | 'query' => Equipment::find()->andWhere(['group' => 'monitor']), 129 | ]), 130 | 'columns' => [ 131 | [ 132 | 'attribute' => 'name', 133 | ], 134 | [ 135 | 'attribute' => 'price', 136 | ], 137 | ], 138 | ]))->render(); // call `render()` to create a single worksheet 139 | 140 | $exporter->configure([ // update spreadsheet configuration 141 | 'title' => 'Mouses', 142 | 'dataProvider' => new ActiveDataProvider([ 143 | 'query' => Equipment::find()->andWhere(['group' => 'mouse']), 144 | ]), 145 | ])->render(); // call `render()` to create a single worksheet 146 | 147 | $exporter->configure([ // update spreadsheet configuration 148 | 'title' => 'Keyboards', 149 | 'dataProvider' => new ActiveDataProvider([ 150 | 'query' => Equipment::find()->andWhere(['group' => 'keyboard']), 151 | ]), 152 | ])->render(); // call `render()` to create a single worksheet 153 | 154 | $exporter->save('/path/to/file.xls'); 155 | ``` 156 | 157 | As the result you will get a single *.xls file with 3 worksheets (tabs): 'Monitors', 'Mouses' and 'Keyboards'. 158 | 159 | Using `\yii2tech\spreadsheet\Spreadsheet::configure()` you can reset any spreadsheet parameter, including `columns`. 160 | Thus you are able to combine several entirely different sheets into a single file. 161 | 162 | 163 | ### Large data processing 164 | 165 | `\yii2tech\spreadsheet\Spreadsheet` allows exporting of the `\yii\data\DataProviderInterface` and `\yii\db\QueryInterface` instances. 166 | Export is performed via batches, which allows processing of the large data without memory overflow. 167 | 168 | In case of `\yii\data\DataProviderInterface` usage, data will be split to batches using pagination mechanism. 169 | Thus you should setup pagination with page size in order to control batch size: 170 | 171 | ```php 172 | new ActiveDataProvider([ 179 | 'query' => Item::find(), 180 | 'pagination' => [ 181 | 'pageSize' => 100, // export batch size 182 | ], 183 | ]), 184 | ]); 185 | $exporter->save('/path/to/file.xls'); 186 | ``` 187 | 188 | > Note: if you disable pagination in your data provider - no batch processing will be performed. 189 | 190 | In case of `\yii\db\QueryInterface` usage, `Spreadsheet` will attempt to use `batch()` method, if it is present in the query 191 | class (for example in case `\yii\db\Query` or `\yii\db\ActiveQuery` usage). If `batch()` method is not available - 192 | `yii\data\ActiveDataProvider` instance will be automatically created around given query. 193 | You can control batch size via `\yii2tech\spreadsheet\Spreadsheet::$batchSize`: 194 | 195 | ```php 196 | Item::find(), 203 | 'batchSize' => 200, // export batch size 204 | ]); 205 | $exporter->save('/path/to/file.xls'); 206 | ``` 207 | 208 | > Note: despite batch data processing reduces amount of resources needed for spreadsheet file generation, 209 | your program may still easily end up with PHP memory limit error on large data. This happens because of 210 | large complexity of the created document, which is stored in the memory during the entire process. 211 | In case you need to export really large data set, consider doing so via simple CSV data format 212 | using [yii2tech/csv-grid](https://github.com/yii2tech/csv-grid) extension. 213 | 214 | 215 | ### Complex headers 216 | 217 | You may union some columns in the sheet header into a groups. For example: you may have 2 different data columns: 218 | 'Planned Revenue' and 'Actual Revenue'. In this case you may want to display them as a single column 'Revenue', split 219 | into 2 sub columns: 'Planned' and 'Actual'. 220 | This can be achieved using `\yii2tech\spreadsheet\Spreadsheet::$headerColumnUnions`. Its each entry 221 | should specify 'offset', which determines the amount of columns to be skipped, and 'length', which determines 222 | the amount of columns to be united. Other options of the union are the same as for regular column. 223 | For example: 224 | 225 | ```php 226 | new ArrayDataProvider([ 233 | 'allModels' => [ 234 | [ 235 | 'column1' => '1.1', 236 | 'column2' => '1.2', 237 | 'column3' => '1.3', 238 | 'column4' => '1.4', 239 | 'column5' => '1.5', 240 | 'column6' => '1.6', 241 | 'column7' => '1.7', 242 | ], 243 | [ 244 | 'column1' => '2.1', 245 | 'column2' => '2.2', 246 | 'column3' => '2.3', 247 | 'column4' => '2.4', 248 | 'column5' => '2.5', 249 | 'column6' => '2.6', 250 | 'column7' => '2.7', 251 | ], 252 | ], 253 | ]), 254 | 'headerColumnUnions' => [ 255 | [ 256 | 'header' => 'Skip 1 column and group 2 next', 257 | 'offset' => 1, 258 | 'length' => 2, 259 | ], 260 | [ 261 | 'header' => 'Skip 2 columns and group 2 next', 262 | 'offset' => 2, 263 | 'length' => 2, 264 | ], 265 | ], 266 | ]); 267 | $exporter->save('/path/to/file.xls'); 268 | ``` 269 | 270 | > Note: only single level of header column unions is supported. You will need to deal with more complex 271 | cases on your own. 272 | 273 | 274 | ### Custom cell rendering 275 | 276 | Before `save()` or `send()` method is invoked, you are able to edit generated spreadsheet, making some 277 | final adjustments to it. Several methods exist to facilitate this process: 278 | 279 | - `\yii2tech\spreadsheet\Spreadsheet::renderCell()` - renders specified cell with given content and style. 280 | - `\yii2tech\spreadsheet\Spreadsheet::applyCellStyle()` - applies specified style to the cell. 281 | - `\yii2tech\spreadsheet\Spreadsheet::mergeCells()` - merges sell range into single one. 282 | 283 | You may use these methods, after document has been composed via `\yii2tech\spreadsheet\Spreadsheet::render()`, 284 | to override or add some content. For example: 285 | 286 | ```php 287 | new ArrayDataProvider([ 295 | 'allModels' => [ 296 | [ 297 | 'id' => 1, 298 | 'name' => 'first', 299 | ], 300 | [ 301 | 'id' => 2, 302 | 'name' => 'second', 303 | ], 304 | ], 305 | ]), 306 | 'columns' => [ 307 | [ 308 | 'class' => SerialColumn::class, 309 | ], 310 | [ 311 | 'attribute' => 'id', 312 | ], 313 | [ 314 | 'attribute' => 'name', 315 | ], 316 | ], 317 | ])->render(); // render the document 318 | 319 | // override serial column header : 320 | $exporter->renderCell('A1', 'Overridden serial column header'); 321 | 322 | // add custom footer : 323 | $exporter->renderCell('A4', 'Custom A4', [ 324 | 'font' => [ 325 | 'color' => [ 326 | 'rgb' => '#FF0000', 327 | ], 328 | ], 329 | ]); 330 | 331 | // merge footer cells : 332 | $exporter->mergeCells('A4:B4'); 333 | 334 | $exporter->save('/path/to/file.xls'); 335 | ``` 336 | 337 | > Tip: you can use `\yii2tech\spreadsheet\Spreadsheet::$rowIndex` to get number of the row, which is next 338 | to the last rendered one. 339 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yii2tech/spreadsheet", 3 | "description": "Yii2 extension for export to a spreadsheet, e.g. Excel, LibreOffice etc.", 4 | "keywords": ["yii2", "spreadsheet", "excel", "libreoffice", "export", "xls", "xlsx"], 5 | "type": "yii2-extension", 6 | "license": "BSD-3-Clause", 7 | "support": { 8 | "issues": "https://github.com/yii2tech/spreadsheet/issues", 9 | "forum": "http://www.yiiframework.com/forum/", 10 | "wiki": "https://github.com/yii2tech/spreadsheet/wiki", 11 | "source": "https://github.com/yii2tech/spreadsheet" 12 | }, 13 | "authors": [ 14 | { 15 | "name": "Paul Klimov", 16 | "email": "klimov.paul@gmail.com" 17 | } 18 | ], 19 | "require": { 20 | "yiisoft/yii2": "~2.0.13", 21 | "phpoffice/phpspreadsheet": "~1.1" 22 | }, 23 | "require-dev": { 24 | "phpunit/phpunit": "^5.0 || ^6.0" 25 | }, 26 | "repositories": [ 27 | { 28 | "type": "composer", 29 | "url": "https://asset-packagist.org" 30 | } 31 | ], 32 | "autoload": { 33 | "psr-4": { "yii2tech\\spreadsheet\\": "src" } 34 | }, 35 | "extra": { 36 | "branch-alias": { 37 | "dev-master": "1.0.x-dev" 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/Column.php: -------------------------------------------------------------------------------- 1 | 17 | * @since 1.0 18 | */ 19 | class Column extends BaseObject 20 | { 21 | /** 22 | * @var Spreadsheet the exporter object that owns this column. 23 | */ 24 | public $grid; 25 | /** 26 | * @var string the header cell content. 27 | */ 28 | public $header; 29 | /** 30 | * @var string the footer cell content. 31 | */ 32 | public $footer; 33 | /** 34 | * @var callable This is a callable that will be used to generate the content of each cell. 35 | * The signature of the function should be the following: `function ($model, $key, $index, $column)`. 36 | * Where `$model`, `$key`, and `$index` refer to the model, key and index of the row currently being rendered 37 | * and `$column` is a reference to the {@see Column} object. 38 | */ 39 | public $content; 40 | /** 41 | * @var bool whether this column is visible. Defaults to true. 42 | */ 43 | public $visible = true; 44 | /** 45 | * @var array the column dimension options. Each option name will be converted into a 'setter' method of {@see \PhpOffice\PhpSpreadsheet\Worksheet\ColumnDimension}. 46 | * @see \PhpOffice\PhpSpreadsheet\Worksheet\ColumnDimension for details on how style configuration is processed. 47 | */ 48 | public $dimensionOptions = []; 49 | /** 50 | * @var array the style for the header cell. 51 | * @see \PhpOffice\PhpSpreadsheet\Style\Style::applyFromArray() for details on how style configuration is processed. 52 | */ 53 | public $headerOptions = []; 54 | /** 55 | * @var array|\Closure the style for the data cell This can either be an array of style 56 | * configuration or an anonymous function ({@see Closure}) that returns such an array. 57 | * The signature of the function should be the following: `function ($model, $key, $index, $column)`. 58 | * Where `$model`, `$key`, and `$index` refer to the model, key and index of the row currently being rendered 59 | * and `$column` is a reference to the {@see Column} object. 60 | * A function may be used to assign different attributes to different rows based on the data in that row. 61 | * 62 | * @see \PhpOffice\PhpSpreadsheet\Style\Style::applyFromArray() for details on how style configuration is processed. 63 | * @see \PhpOffice\PhpSpreadsheet\Style\Alignment::applyFromArray() for details on how 'alignment' configuration is processed. 64 | */ 65 | public $contentOptions = []; 66 | /** 67 | * @var array the style for the footer cell. 68 | * @see \PhpOffice\PhpSpreadsheet\Style\Style::applyFromArray() for details on how style configuration is processed. 69 | * @see \PhpOffice\PhpSpreadsheet\Style\Alignment::applyFromArray() for details on how 'alignment' configuration is processed. 70 | */ 71 | public $footerOptions = []; 72 | /** 73 | * @var array the style for the filter cell. 74 | * @see \PhpOffice\PhpSpreadsheet\Style\Style::applyFromArray() for details on how style configuration is processed. 75 | * @see \PhpOffice\PhpSpreadsheet\Style\Alignment::applyFromArray() for details on how 'alignment' configuration is processed. 76 | */ 77 | public $filterOptions = []; 78 | 79 | 80 | /** 81 | * Renders the header cell. 82 | * @param string $cell cell coordinates. 83 | */ 84 | public function renderHeaderCell($cell) 85 | { 86 | $this->grid->renderCell($cell, $this->renderHeaderCellContent(), $this->headerOptions); 87 | } 88 | 89 | /** 90 | * Renders the footer cell. 91 | * @param string $cell cell coordinates. 92 | */ 93 | public function renderFooterCell($cell) 94 | { 95 | $this->grid->renderCell($cell, $this->renderFooterCellContent(), $this->footerOptions); 96 | } 97 | 98 | /** 99 | * Renders a data cell. 100 | * @param string $cell cell coordinates. 101 | * @param mixed $model the data model being rendered 102 | * @param mixed $key the key associated with the data model 103 | * @param int $index the zero-based index of the data item among the item array returned by {@see GridView::dataProvider}. 104 | */ 105 | public function renderDataCell($cell, $model, $key, $index) 106 | { 107 | if ($this->contentOptions instanceof Closure) { 108 | $style = call_user_func($this->contentOptions, $model, $key, $index, $this); 109 | } else { 110 | $style = $this->contentOptions; 111 | } 112 | 113 | $this->grid->renderCell($cell, $this->renderDataCellContent($model, $key, $index), $style); 114 | } 115 | 116 | /** 117 | * Renders the filter cell. 118 | * @param string $cell cell coordinates. 119 | */ 120 | public function renderFilterCell($cell) 121 | { 122 | $this->grid->renderCell($cell, $this->renderFilterCellContent(), $this->filterOptions); 123 | } 124 | 125 | /** 126 | * Renders the header cell content. 127 | * The default implementation simply renders {@see Column::$header}. 128 | * This method may be overridden to customize the rendering of the header cell. 129 | * @return string the rendering result 130 | */ 131 | public function renderHeaderCellContent() 132 | { 133 | return trim($this->header) !== '' ? $this->header : $this->grid->emptyCell; 134 | } 135 | 136 | /** 137 | * Renders the footer cell content. 138 | * The default implementation simply renders {@see Column::$footer}. 139 | * This method may be overridden to customize the rendering of the footer cell. 140 | * @return string the rendering result 141 | */ 142 | public function renderFooterCellContent() 143 | { 144 | return trim($this->footer) !== '' ? $this->footer : $this->grid->emptyCell; 145 | } 146 | 147 | /** 148 | * Renders the data cell content. 149 | * @param mixed $model the data model 150 | * @param mixed $key the key associated with the data model 151 | * @param int $index the zero-based index of the data model among the models array returned by {@see Spreadsheet::$dataProvider}. 152 | * @return string the rendering result 153 | */ 154 | public function renderDataCellContent($model, $key, $index) 155 | { 156 | if ($this->content === null) { 157 | return $this->grid->emptyCell; 158 | } 159 | return call_user_func($this->content, $model, $key, $index, $this); 160 | } 161 | 162 | /** 163 | * Renders the filter cell content. 164 | * The default implementation simply renders a space. 165 | * This method may be overridden to customize the rendering of the filter cell (if any). 166 | * @return string the rendering result 167 | */ 168 | public function renderFilterCellContent() 169 | { 170 | return $this->grid->emptyCell; 171 | } 172 | } -------------------------------------------------------------------------------- /src/DataColumn.php: -------------------------------------------------------------------------------- 1 | 20 | * @since 1.0 21 | */ 22 | class DataColumn extends Column 23 | { 24 | /** 25 | * @var string the attribute name associated with this column. When neither {@see Column::$content} nor {@see value} 26 | * is specified, the value of the specified attribute will be retrieved from each data model and displayed. 27 | * 28 | * Also, if {@see label} is not specified, the label associated with the attribute will be displayed. 29 | */ 30 | public $attribute; 31 | /** 32 | * @var string label to be displayed in the {@see Column::$header} and also to be used as the sorting 33 | * link label when sorting is enabled for this column. 34 | * If it is not set and the models provided by the GridViews data provider are instances 35 | * of {@see \yii\db\ActiveRecord}, the label will be determined using {@see \yii\db\ActiveRecord::getAttributeLabel()}. 36 | * Otherwise {@see \yii\helpers\Inflector::camel2words()} will be used to get a label. 37 | */ 38 | public $label; 39 | /** 40 | * @var string|\Closure an anonymous function or a string that is used to determine the value to display in the current column. 41 | * 42 | * If this is an anonymous function, it will be called for each row and the return value will be used as the value to 43 | * display for every data model. The signature of this function should be: `function ($model, $key, $index, $column)`. 44 | * Where `$model`, `$key`, and `$index` refer to the model, key and index of the row currently being rendered 45 | * and `$column` is a reference to the {@see DataColumn} object. 46 | * 47 | * You may also set this property to a string representing the attribute name to be displayed in this column. 48 | * This can be used when the attribute to be displayed is different from the {@see attribute} that is used for 49 | * sorting and filtering. 50 | * 51 | * If this is not set, `$model[$attribute]` will be used to obtain the value, where `$attribute` is the value of {@see attribute}. 52 | */ 53 | public $value; 54 | /** 55 | * @var string|array in which format should the value of each data model be displayed as (e.g. `"raw"`, `"text"`, `"html"`, 56 | * `['date', 'php:Y-m-d']`). Supported formats are determined by the {@see Spreadsheet::$formatter} used by 57 | * the {@see Spreadsheet}. Default format is "raw" which will display value as it is. 58 | */ 59 | public $format = 'raw'; 60 | /** 61 | * @var string|array|bool the HTML code representing a filter input (e.g. a text field, a dropdown list) 62 | * that is used for this data column. This property is effective only when {@see Spreadsheet::$filterModel} is set. 63 | * 64 | * - If this property is not set, a text field will be generated as the filter input; 65 | * - If this property is an array, a dropdown list will be generated that uses this property value as 66 | * the list options. 67 | * - If you don't want a filter for this data column, set this value to be false. 68 | */ 69 | public $filter; 70 | 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | public function renderHeaderCellContent() 76 | { 77 | if ($this->header !== null || $this->label === null && $this->attribute === null) { 78 | return parent::renderHeaderCellContent(); 79 | } 80 | 81 | $provider = $this->grid->dataProvider; 82 | 83 | if ($this->label === null) { 84 | if ($provider instanceof ActiveDataProvider && $provider->query instanceof ActiveQueryInterface) { 85 | /* @var $model Model */ 86 | $model = new $provider->query->modelClass; 87 | $label = $model->getAttributeLabel($this->attribute); 88 | } else { 89 | $models = $provider->getModels(); 90 | if (($model = reset($models)) instanceof Model) { 91 | /* @var $model Model */ 92 | $label = $model->getAttributeLabel($this->attribute); 93 | } else { 94 | $label = Inflector::camel2words($this->attribute); 95 | } 96 | } 97 | } else { 98 | $label = $this->label; 99 | } 100 | 101 | return $label; 102 | } 103 | 104 | /** 105 | * {@inheritdoc} 106 | */ 107 | public function renderFilterCellContent() 108 | { 109 | if (is_string($this->filter)) { 110 | return $this->filter; 111 | } 112 | 113 | return parent::renderFilterCellContent(); 114 | } 115 | 116 | /** 117 | * Returns the data cell value. 118 | * @param mixed $model the data model 119 | * @param mixed $key the key associated with the data model 120 | * @param int $index the zero-based index of the data model among the models array returned by {@see Spreadsheet::$dataProvider}. 121 | * @return string the data cell value 122 | */ 123 | public function getDataCellValue($model, $key, $index) 124 | { 125 | if ($this->value !== null) { 126 | if (is_string($this->value)) { 127 | return ArrayHelper::getValue($model, $this->value); 128 | } 129 | return call_user_func($this->value, $model, $key, $index, $this); 130 | } elseif ($this->attribute !== null) { 131 | return ArrayHelper::getValue($model, $this->attribute); 132 | } 133 | 134 | return null; 135 | } 136 | 137 | /** 138 | * {@inheritdoc} 139 | */ 140 | public function renderDataCellContent($model, $key, $index) 141 | { 142 | if ($this->content === null) { 143 | $value = $this->getDataCellValue($model, $key, $index); 144 | if ($value === null) { 145 | return $this->grid->nullDisplay; 146 | } 147 | return $this->grid->formatter->format($value, $this->format); 148 | } 149 | 150 | return parent::renderDataCellContent($model, $key, $index); 151 | } 152 | } -------------------------------------------------------------------------------- /src/SerialColumn.php: -------------------------------------------------------------------------------- 1 | [ 17 | * [ 18 | * 'class' => \yii2tech\spreadsheet\SerialColumn::class, 19 | * ], 20 | * // ... 21 | * ] 22 | * ``` 23 | * 24 | * @author Paul Klimov 25 | * @since 1.0 26 | */ 27 | class SerialColumn extends Column 28 | { 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public $header = '#'; 33 | 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function renderDataCellContent($model, $key, $index) 39 | { 40 | return $index + 1; 41 | } 42 | } -------------------------------------------------------------------------------- /src/Spreadsheet.php: -------------------------------------------------------------------------------- 1 | new ArrayDataProvider([ 32 | * 'allModels' => [ 33 | * [ 34 | * 'name' => 'some name', 35 | * 'price' => '9879', 36 | * ], 37 | * [ 38 | * 'name' => 'name 2', 39 | * 'price' => '79', 40 | * ], 41 | * ], 42 | * ]), 43 | * 'columns' => [ 44 | * [ 45 | * 'attribute' => 'name', 46 | * 'contentOptions' => [ 47 | * 'alignment' => [ 48 | * 'horizontal' => 'center', 49 | * 'vertical' => 'center', 50 | * ], 51 | * ], 52 | * ], 53 | * [ 54 | * 'attribute' => 'price', 55 | * ], 56 | * ], 57 | * ]); 58 | * $exporter->save('/path/to/file.xls'); 59 | * ``` 60 | * 61 | * @see https://phpspreadsheet.readthedocs.io/ 62 | * @see \PhpOffice\PhpSpreadsheet\Spreadsheet 63 | * 64 | * @property array|Formatter $formatter the formatter used to format model attribute values into displayable texts. 65 | * @property \PhpOffice\PhpSpreadsheet\Spreadsheet $document spreadsheet document representation instance. 66 | * 67 | * @author Paul Klimov 68 | * @since 1.0 69 | */ 70 | class Spreadsheet extends Component 71 | { 72 | /** 73 | * @var \yii\data\DataProviderInterface the data provider for the view. This property is required. 74 | */ 75 | public $dataProvider; 76 | /** 77 | * @var \yii\db\QueryInterface the data source query. 78 | * Note: this field will be ignored in case {@see dataProvider} is set. 79 | */ 80 | public $query; 81 | /** 82 | * @var int the number of records to be fetched in each batch. 83 | * This property takes effect only in case of {@see query} usage. 84 | */ 85 | public $batchSize = 100; 86 | /** 87 | * @var array|Column[] spreadsheet column configuration. Each array element represents the configuration 88 | * for one particular column. For example: 89 | * 90 | * ```php 91 | * [ 92 | * ['class' => SerialColumn::class], 93 | * [ 94 | * 'class' => DataColumn::class, // this line is optional 95 | * 'attribute' => 'name', 96 | * 'format' => 'text', 97 | * 'header' => 'Name', 98 | * ], 99 | * ] 100 | * ``` 101 | * 102 | * If a column is of class {@see DataColumn}, the "class" element can be omitted. 103 | */ 104 | public $columns = []; 105 | /** 106 | * @var bool whether to show the header section of the sheet. 107 | */ 108 | public $showHeader = true; 109 | /** 110 | * @var bool whether to show the footer section of the sheet. 111 | */ 112 | public $showFooter = false; 113 | /** 114 | * @var string|null sheet title. 115 | */ 116 | public $title; 117 | /** 118 | * @var string the HTML display when the content of a cell is empty. 119 | * This property is used to render cells that have no defined content, 120 | * e.g. empty footer or filter cells. 121 | * 122 | * Note that this is not used by the {@see DataColumn} if a data item is `null`. In that case 123 | * the {@see nullDisplay} property will be used to indicate an empty data value. 124 | */ 125 | public $emptyCell = ''; 126 | /** 127 | * @var string the text to be displayed when formatting a `null` data value. 128 | */ 129 | public $nullDisplay = ''; 130 | /** 131 | * @var string writer type (format type). If not set, it will be determined automatically. 132 | * Supported values: 133 | * 134 | * - 'Xls' 135 | * - 'Xlsx' 136 | * - 'Ods' 137 | * - 'Csv' 138 | * - 'Html' 139 | * - 'Tcpdf' 140 | * - 'Dompdf' 141 | * - 'Mpdf' 142 | * 143 | * @see IOFactory 144 | */ 145 | public $writerType; 146 | /** 147 | * @var callable|null a PHP callback, which should create spreadsheet writer instance. 148 | * The signature of this callback should be following: `function(\PhpOffice\PhpSpreadsheet\Spreadsheet $spreadsheet, string $writerType): \PhpOffice\PhpSpreadsheet\Writer\IWriter` 149 | * @see \PhpOffice\PhpSpreadsheet\Writer\IWriter 150 | * @since 1.0.5 151 | */ 152 | public $writerCreator; 153 | /** 154 | * @var array[] list of header column unions. 155 | * For example: 156 | * 157 | * ```php 158 | * [ 159 | * [ 160 | * 'header' => 'Skip one column and group 3 next', 161 | * 'offset' => 1, 162 | * 'length' => 3, 163 | * ], 164 | * [ 165 | * 'header' => 'Skip two column and group 5 next', 166 | * 'offset' => 2, 167 | * 'length' => 5, 168 | * ], 169 | * ] 170 | * ``` 171 | */ 172 | public $headerColumnUnions = []; 173 | /** 174 | * @var int|null current sheet row index. 175 | * Value of this field automatically changes during spreadsheet rendering. After rendering is complete, 176 | * it will contain the number of the row next to the latest fill-up one. 177 | * Note: be careful while manually manipulating value of this field as it may cause unexpected results. 178 | */ 179 | public $rowIndex; 180 | /** 181 | * @var int index of the sheet row, from which rendering should start. 182 | * This field can be used to skip some lines at the sheet beginning for the further manual fill up. 183 | * @since 1.0.4 184 | */ 185 | public $startRowIndex = 1; 186 | 187 | /** 188 | * @var bool whether spreadsheet has been already rendered or not. 189 | */ 190 | protected $isRendered = false; 191 | 192 | /** 193 | * @var \PhpOffice\PhpSpreadsheet\Spreadsheet|null spreadsheet document representation instance. 194 | */ 195 | private $_document; 196 | /** 197 | * @var array|Formatter the formatter used to format model attribute values into displayable texts. 198 | * This can be either an instance of {@see Formatter} or an configuration array for creating the {@see Formatter} 199 | * instance. If this property is not set, the "formatter" application component will be used. 200 | */ 201 | private $_formatter; 202 | /** 203 | * @var array|null internal iteration information. 204 | */ 205 | private $batchInfo; 206 | 207 | 208 | /** 209 | * @return \PhpOffice\PhpSpreadsheet\Spreadsheet spreadsheet document representation instance. 210 | */ 211 | public function getDocument() 212 | { 213 | if (!is_object($this->_document)) { 214 | $this->_document = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); 215 | } 216 | return $this->_document; 217 | } 218 | 219 | /** 220 | * @param \PhpOffice\PhpSpreadsheet\Spreadsheet|null $document spreadsheet document representation instance. 221 | */ 222 | public function setDocument($document) 223 | { 224 | $this->_document = $document; 225 | } 226 | 227 | /** 228 | * @return Formatter formatter instance. 229 | */ 230 | public function getFormatter() 231 | { 232 | if (!is_object($this->_formatter)) { 233 | if ($this->_formatter === null) { 234 | $this->_formatter = Yii::$app->getFormatter(); 235 | } else { 236 | $this->_formatter = Instance::ensure($this->_formatter, Formatter::class); 237 | } 238 | } 239 | return $this->_formatter; 240 | } 241 | 242 | /** 243 | * @param array|Formatter $formatter formatter instance. 244 | */ 245 | public function setFormatter($formatter) 246 | { 247 | $this->_formatter = $formatter; 248 | } 249 | 250 | /** 251 | * Creates column objects and initializes them. 252 | */ 253 | protected function initColumns() 254 | { 255 | foreach ($this->columns as $i => $column) { 256 | if (is_string($column)) { 257 | $column = $this->createDataColumn($column); 258 | } elseif (is_array($column)) { 259 | $column = Yii::createObject(array_merge([ 260 | 'class' => DataColumn::class, 261 | 'grid' => $this, 262 | ], $column)); 263 | } 264 | if (!$column->visible) { 265 | unset($this->columns[$i]); 266 | continue; 267 | } 268 | $this->columns[$i] = $column; 269 | } 270 | } 271 | 272 | /** 273 | * This function tries to guess the columns to show from the given data 274 | * if {@see columns} are not explicitly specified. 275 | * @param \yii\base\Model|array $model model to be used for column information source. 276 | */ 277 | protected function guessColumns($model) 278 | { 279 | if (is_array($model) || is_object($model)) { 280 | foreach ($model as $name => $value) { 281 | $this->columns[] = (string) $name; 282 | } 283 | } 284 | } 285 | 286 | /** 287 | * Creates a {@see DataColumn} object based on a string in the format of "attribute:format:label". 288 | * @param string $text the column specification string 289 | * @return DataColumn the column instance 290 | * @throws InvalidConfigException if the column specification is invalid 291 | */ 292 | protected function createDataColumn($text) 293 | { 294 | if (!preg_match('/^([^:]+)(:(\w*))?(:(.*))?$/', $text, $matches)) { 295 | throw new InvalidConfigException('The column must be specified in the format of "attribute", "attribute:format" or "attribute:format:label"'); 296 | } 297 | 298 | return Yii::createObject([ 299 | 'class' => DataColumn::class, 300 | 'grid' => $this, 301 | 'attribute' => $matches[1], 302 | 'format' => isset($matches[3]) ? $matches[3] : 'raw', 303 | 'label' => isset($matches[5]) ? $matches[5] : null, 304 | ]); 305 | } 306 | 307 | /** 308 | * Sets spreadsheet document properties. 309 | * @param array $properties list of document properties in format: name => value 310 | * @return $this self reference. 311 | * @see \PhpOffice\PhpSpreadsheet\Document\Properties 312 | */ 313 | public function properties($properties) 314 | { 315 | $documentProperties = $this->getDocument()->getProperties(); 316 | foreach ($properties as $name => $value) { 317 | $method = 'set' . ucfirst($name); 318 | call_user_func([$documentProperties, $method], $value); 319 | } 320 | return $this; 321 | } 322 | 323 | /** 324 | * Configures (re-configures) this spreadsheet with the property values. 325 | * This method is useful for rendering multisheet documents. For example: 326 | * 327 | * ```php 328 | * (new Spreadsheet([ 329 | * 'title' => 'Monitors', 330 | * 'dataProvider' => $monitorDataProvider, 331 | * ])) 332 | * ->render() 333 | * ->configure([ 334 | * 'title' => 'Mouses', 335 | * 'dataProvider' => $mouseDataProvider, 336 | * ]) 337 | * ->render() 338 | * ->configure([ 339 | * 'title' => 'Keyboards', 340 | * 'dataProvider' => $keyboardDataProvider, 341 | * ]) 342 | * ->save('/path/to/export/files/office-equipment.xls'); 343 | * ``` 344 | * 345 | * @param array $properties the property initial values given in terms of name-value pairs. 346 | * @return $this self reference. 347 | */ 348 | public function configure($properties) 349 | { 350 | Yii::configure($this, $properties); 351 | return $this; 352 | } 353 | 354 | /** 355 | * Performs actual document composition. 356 | * @return $this self reference. 357 | */ 358 | public function render() 359 | { 360 | if ($this->dataProvider === null) { 361 | if ($this->query !== null) { 362 | $this->dataProvider = new ActiveDataProvider([ 363 | 'query' => $this->query, 364 | 'pagination' => [ 365 | 'pageSize' => $this->batchSize, 366 | ], 367 | ]); 368 | } 369 | } 370 | 371 | $document = $this->getDocument(); 372 | 373 | if ($this->isRendered) { 374 | // second run 375 | $document->createSheet(); 376 | $document->setActiveSheetIndex($document->getActiveSheetIndex() + 1); 377 | } 378 | 379 | if ($this->title !== null) { 380 | $document->getActiveSheet()->setTitle($this->title); 381 | } 382 | 383 | $this->rowIndex = $this->startRowIndex; 384 | 385 | $columnsInitialized = false; 386 | $modelIndex = 0; 387 | while (($data = $this->batchModels()) !== false) { 388 | list($models, $keys) = $data; 389 | 390 | if (!$columnsInitialized) { 391 | if (empty($this->columns)) { 392 | $this->guessColumns(reset($models)); 393 | } 394 | 395 | $this->initColumns(); 396 | $this->applyColumnOptions(); 397 | $columnsInitialized = true; 398 | 399 | if ($this->showHeader) { 400 | $this->renderHeader(); 401 | } 402 | } 403 | 404 | $this->renderBody($models, $keys, $modelIndex); 405 | $this->gc(); 406 | } 407 | 408 | if ($this->showFooter) { 409 | $this->renderFooter(); 410 | } 411 | 412 | $this->isRendered = true; 413 | 414 | return $this; 415 | } 416 | 417 | /** 418 | * Renders sheet table body batch. 419 | * This method will be invoked several times, one per each model batch. 420 | * @param array $models batch of models. 421 | * @param array $keys batch of model keys. 422 | * @param int $modelIndex model iteration index. 423 | */ 424 | protected function renderBody($models, $keys, &$modelIndex) 425 | { 426 | foreach ($models as $index => $model) { 427 | $key = isset($keys[$index]) ? $keys[$index] : $index; 428 | $columnIndex = 'A'; 429 | foreach ($this->columns as $column) { 430 | /* @var $column Column */ 431 | $column->renderDataCell($columnIndex . $this->rowIndex, $model, $key, $modelIndex); 432 | $columnIndex++; 433 | } 434 | $this->rowIndex++; 435 | $modelIndex++; 436 | } 437 | } 438 | 439 | /** 440 | * Renders sheet table header 441 | */ 442 | protected function renderHeader() 443 | { 444 | if (empty($this->headerColumnUnions)) { 445 | $columnIndex = 'A'; 446 | foreach ($this->columns as $column) { 447 | /* @var $column Column */ 448 | $column->renderHeaderCell($columnIndex . $this->rowIndex); 449 | $columnIndex++; 450 | } 451 | $this->rowIndex++; 452 | return; 453 | } 454 | 455 | $sheet = $this->getDocument()->getActiveSheet(); 456 | 457 | $columns = $this->columns; 458 | 459 | $columnIndex = 'A'; 460 | foreach ($this->headerColumnUnions as $columnUnion) { 461 | if (isset($columnUnion['offset'])) { 462 | $offset = (int)$columnUnion['offset']; 463 | unset($columnUnion['offset']); 464 | } else { 465 | $offset = 0; 466 | } 467 | 468 | if (isset($columnUnion['length'])) { 469 | $length = (int)$columnUnion['length']; 470 | unset($columnUnion['length']); 471 | } else { 472 | $length = 1; 473 | } 474 | 475 | while ($offset > 0) { 476 | /* @var $column Column */ 477 | $column = array_shift($columns); 478 | $column->renderHeaderCell($columnIndex . $this->rowIndex); 479 | 480 | $sheet->mergeCells($columnIndex . ($this->rowIndex) . ':' . $columnIndex . ($this->rowIndex + 1)); 481 | $columnIndex++; 482 | $offset--; 483 | } 484 | 485 | $column = new Column($columnUnion); 486 | $column->grid = $this; 487 | $column->renderHeaderCell($columnIndex . $this->rowIndex); 488 | 489 | $startColumnIndex = $columnIndex; 490 | while (true) { 491 | /* @var $column Column */ 492 | $column = array_shift($columns); 493 | $column->renderHeaderCell($columnIndex . ($this->rowIndex + 1)); 494 | $length--; 495 | if (($length < 1)) { 496 | break; 497 | } 498 | $columnIndex++; 499 | } 500 | 501 | $sheet->mergeCells($startColumnIndex . $this->rowIndex . ':' . $columnIndex . $this->rowIndex); 502 | 503 | $columnIndex++; 504 | } 505 | 506 | foreach ($columns as $column) { 507 | /* @var $column Column */ 508 | $column->renderHeaderCell($columnIndex . $this->rowIndex); 509 | $sheet->mergeCells($columnIndex . ($this->rowIndex) . ':' . $columnIndex . ($this->rowIndex + 1)); 510 | $columnIndex++; 511 | } 512 | 513 | $this->rowIndex++; 514 | $this->rowIndex++; 515 | } 516 | 517 | /** 518 | * Renders sheet table footer 519 | */ 520 | protected function renderFooter() 521 | { 522 | $columnIndex = 'A'; 523 | foreach ($this->columns as $column) { 524 | /* @var $column Column */ 525 | $column->renderFooterCell($columnIndex . $this->rowIndex); 526 | $columnIndex++; 527 | } 528 | $this->rowIndex++; 529 | } 530 | 531 | /** 532 | * Applies column overall options, such as dimension options. 533 | */ 534 | protected function applyColumnOptions() 535 | { 536 | $sheet = $this->getDocument()->getActiveSheet(); 537 | $columnIndex = 'A'; 538 | foreach ($this->columns as $column) { 539 | /* @var $column Column */ 540 | if (!empty($column->dimensionOptions)) { 541 | $columnDimension = $sheet->getColumnDimension($columnIndex); 542 | foreach ($column->dimensionOptions as $name => $value) { 543 | $method = 'set' . ucfirst($name); 544 | call_user_func([$columnDimension, $method], $value); 545 | } 546 | } 547 | 548 | $columnIndex++; 549 | } 550 | } 551 | 552 | /** 553 | * Iterates over {@see query} or {@see dataProvider} returning data by batches. 554 | * @return array|false data batch: first element - models list, second model keys list. 555 | */ 556 | protected function batchModels() 557 | { 558 | if ($this->batchInfo === null) { 559 | if ($this->query !== null && method_exists($this->query, 'batch')) { 560 | $this->batchInfo = [ 561 | 'queryIterator' => $this->query->batch($this->batchSize) 562 | ]; 563 | } else { 564 | $this->batchInfo = [ 565 | 'pagination' => $this->dataProvider->getPagination(), 566 | 'page' => 0 567 | ]; 568 | } 569 | } 570 | 571 | if (isset($this->batchInfo['queryIterator'])) { 572 | /* @var $iterator \Iterator */ 573 | $iterator = $this->batchInfo['queryIterator']; 574 | $iterator->next(); 575 | 576 | if ($iterator->valid()) { 577 | return [$iterator->current(), []]; 578 | } 579 | 580 | $this->batchInfo = null; 581 | return false; 582 | } 583 | 584 | if (isset($this->batchInfo['pagination'])) { 585 | /* @var $pagination \yii\data\Pagination|bool */ 586 | $pagination = $this->batchInfo['pagination']; 587 | $page = $this->batchInfo['page']; 588 | 589 | if ($pagination === false || $pagination->pageCount === 0) { 590 | if ($page === 0) { 591 | $this->batchInfo['page']++; 592 | return [ 593 | $this->dataProvider->getModels(), 594 | $this->dataProvider->getKeys() 595 | ]; 596 | } 597 | } else { 598 | if ($page < $pagination->pageCount) { 599 | $pagination->setPage($page); 600 | $this->dataProvider->prepare(true); 601 | $this->batchInfo['page']++; 602 | return [ 603 | $this->dataProvider->getModels(), 604 | $this->dataProvider->getKeys() 605 | ]; 606 | } 607 | } 608 | 609 | $this->batchInfo = null; 610 | return false; 611 | } 612 | 613 | return false; 614 | } 615 | 616 | /** 617 | * Renders cell with given coordinates. 618 | * @param string $cell cell coordinates, e.g. 'A1', 'B4' etc. 619 | * @param string $content cell raw content. 620 | * @param array $style cell style options. 621 | * @return $this self reference. 622 | */ 623 | public function renderCell($cell, $content, $style = []) 624 | { 625 | $sheet = $this->getDocument()->getActiveSheet(); 626 | $sheet->setCellValue($cell, $content); 627 | $this->applyCellStyle($cell, $style); 628 | return $this; 629 | } 630 | 631 | /** 632 | * Applies cell style from configuration. 633 | * @param string $cell cell coordinates, e.g. 'A1', 'B4' etc. 634 | * @param array $style style configuration. 635 | * @return $this self reference. 636 | * @throws \PhpOffice\PhpSpreadsheet\Exception on failure. 637 | */ 638 | public function applyCellStyle($cell, $style) 639 | { 640 | if (empty($style)) { 641 | return $this; 642 | } 643 | 644 | $cellStyle = $this->getDocument()->getActiveSheet()->getStyle($cell); 645 | if (isset($style['alignment'])) { 646 | $cellStyle->getAlignment()->applyFromArray($style['alignment']); 647 | unset($style['alignment']); 648 | if (empty($style)) { 649 | return $this; 650 | } 651 | } 652 | $cellStyle->applyFromArray($style); 653 | 654 | return $this; 655 | } 656 | 657 | /** 658 | * Merges sell range into single one. 659 | * @param string $cellRange cell range (e.g. 'A1:E1'). 660 | * @return $this self reference. 661 | * @throws \PhpOffice\PhpSpreadsheet\Exception on failure. 662 | */ 663 | public function mergeCells($cellRange) 664 | { 665 | $this->getDocument()->getActiveSheet()->mergeCells($cellRange); 666 | return $this; 667 | } 668 | 669 | /** 670 | * Saves the document into a file. 671 | * @param string $filename name of the output file. 672 | */ 673 | public function save($filename) 674 | { 675 | if (!$this->isRendered) { 676 | $this->render(); 677 | } 678 | 679 | $filename = Yii::getAlias($filename); 680 | 681 | $writerType = $this->writerType; 682 | if ($writerType === null) { 683 | $fileExtension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); 684 | $writerType = ucfirst($fileExtension); 685 | } 686 | 687 | $fileDir = pathinfo($filename, PATHINFO_DIRNAME); 688 | FileHelper::createDirectory($fileDir); 689 | 690 | $writer = $this->createWriter($writerType); 691 | $writer->save($filename); 692 | } 693 | 694 | /** 695 | * Sends the rendered content as a file to the browser. 696 | * 697 | * Note that this method only prepares the response for file sending. The file is not sent 698 | * until {@see \yii\web\Response::send()} is called explicitly or implicitly. 699 | * The latter is done after you return from a controller action. 700 | * 701 | * @param string $attachmentName the file name shown to the user. 702 | * @param array $options additional options for sending the file. The following options are supported: 703 | * 704 | * - `mimeType`: the MIME type of the content. Defaults to 'application/octet-stream'. 705 | * - `inline`: bool, whether the browser should open the file within the browser window. Defaults to false, 706 | * meaning a download dialog will pop up. 707 | * 708 | * @return \yii\web\Response the response object. 709 | */ 710 | public function send($attachmentName, $options = []) 711 | { 712 | if (!$this->isRendered) { 713 | $this->render(); 714 | } 715 | 716 | $writerType = $this->writerType; 717 | if ($writerType === null) { 718 | $fileExtension = strtolower(pathinfo($attachmentName, PATHINFO_EXTENSION)); 719 | $writerType = ucfirst($fileExtension); 720 | } 721 | 722 | $tmpResource = tmpfile(); 723 | if ($tmpResource === false) { 724 | throw new \RuntimeException('Unable to create temporary file.'); 725 | } 726 | 727 | $tmpResourceMetaData = stream_get_meta_data($tmpResource); 728 | $tmpFileName = $tmpResourceMetaData['uri']; 729 | 730 | $writer = $this->createWriter($writerType); 731 | $writer->save($tmpFileName); 732 | unset($writer); 733 | 734 | $tmpFileStatistics = fstat($tmpResource); 735 | if ($tmpFileStatistics['size'] > 0) { 736 | return Yii::$app->getResponse()->sendStreamAsFile($tmpResource, $attachmentName, $options); 737 | } 738 | 739 | // some writers, like 'Xlsx', may delete target file during the process, making temporary file resource invalid 740 | $response = Yii::$app->getResponse(); 741 | $response->on(Response::EVENT_AFTER_SEND, function() use ($tmpResource) { 742 | // with temporary file resource closing file matching its URI will be deleted, even if resource is invalid 743 | fclose($tmpResource); 744 | }); 745 | 746 | return $response->sendFile($tmpFileName, $attachmentName, $options); 747 | } 748 | 749 | /** 750 | * Performs PHP memory garbage collection. 751 | */ 752 | protected function gc() 753 | { 754 | if (!gc_enabled()) { 755 | gc_enable(); 756 | } 757 | gc_collect_cycles(); 758 | } 759 | 760 | /** 761 | * Creates a spreadsheet writer for the given type. 762 | * @param string $writerType spreadsheet writer type. 763 | * @return \PhpOffice\PhpSpreadsheet\Writer\IWriter 764 | * @since 1.0.5 765 | */ 766 | protected function createWriter($writerType) 767 | { 768 | if ($this->writerCreator === null) { 769 | return IOFactory::createWriter($this->getDocument(), $writerType); 770 | } 771 | 772 | return call_user_func($this->writerCreator, $this->getDocument(), $writerType); 773 | } 774 | } --------------------------------------------------------------------------------