├── 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 | [](https://packagist.org/packages/yii2tech/spreadsheet)
14 | [](https://packagist.org/packages/yii2tech/spreadsheet)
15 | [](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 | }
--------------------------------------------------------------------------------