├── .gitignore ├── README.md ├── composer.json └── src ├── ActiveExcelSheet.php ├── ExcelFile.php └── ExcelSheet.php /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | composer.lock 3 | /vendor 4 | .vimrc 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Yii2 Excel Export 2 | ================= 3 | 4 | [![Latest Stable Version](https://poser.pugx.org/codemix/yii2-excelexport/v/stable)](https://packagist.org/packages/codemix/yii2-excelexport) 5 | [![Total Downloads](https://poser.pugx.org/codemix/yii2-excelexport/downloads)](https://packagist.org/packages/codemix/yii2-excelexport) 6 | [![Latest Unstable Version](https://poser.pugx.org/codemix/yii2-excelexport/v/unstable)](https://packagist.org/packages/codemix/yii2-excelexport) 7 | [![License](https://poser.pugx.org/codemix/yii2-excelexport/license)](https://packagist.org/packages/codemix/yii2-excelexport) 8 | 9 | > **Note:** The minimum requirement since 2.6.0 is Yii 2.0.13. The latest 10 | > version for older Yii releases is 2.5.0. 11 | 12 | ## Features 13 | 14 | * Export data from `ActiveQuery` results 15 | * Export any other data (Array, Iterable, ...) 16 | * Create excel files with multiple sheets 17 | * Format cells and values 18 | 19 | To write the Excel file, we use the excellent [PHPSpreadsheet](https://github.com/PHPOffice/PhpSpreadsheet) package. 20 | 21 | ## Installation 22 | 23 | Install the package with [composer](http://getcomposer.org): 24 | 25 | composer require codemix/yii2-excelexport 26 | 27 | ## Quickstart example 28 | 29 | ```php 30 | 'codemix\excelexport\ExcelFile', 33 | 'sheets' => [ 34 | 'Users' => [ 35 | 'class' => 'codemix\excelexport\ActiveExcelSheet', 36 | 'query' => User::find(), 37 | ] 38 | ] 39 | ]); 40 | $file->send('user.xlsx'); 41 | ``` 42 | 43 | Find more examples below. 44 | 45 | 46 | ## Configuration and Use 47 | 48 | ### ExcelFile 49 | 50 | Property | Description 51 | ---------|------------- 52 | `writerClass` | The file format as supported by PHPOffice. The default is `\PhpOffice\PhpSpreadsheet\Writer\Xlsx` 53 | `sheets` | An array of sheet configurations (see below). The keys are used as sheet names. 54 | `fileOptions` | Options to pass to the constructor of `mikehaertl\tmp\File`. Available keys are `prefix`, `suffix` and `directory`. 55 | 56 | Methods | Description 57 | ---------|------------- 58 | `saveAs($name)` | Saves the excel file under `$name` 59 | `send($name=null, $inline=false, $contentType = 'application/vnd.ms-excel')` | Sends the excel file to the browser. If `$name` is empty, the file is streamed for inline display, otherwhise a download dialog will open, unless `$inline` is `true` which will force inline display even if a filename is supplied. 60 | `createSheets()` | Only creates the sheets of the excel workbook but does not save the file. This is usually called implicitely on `saveAs()` and `send()` but can also be called manually to modify the sheets before saving. 61 | `getWriter()` | Returns the `\PhpOffice\PhpSpreadsheet\Writer\BaseWrite` instance 62 | `getWorkbook()` | Returns the `\PhpOffice\PhpSpreadsheet\Spreadsheet` workbook instance 63 | `getTmpFile()` | Returns the `mikehaertl\tmp\File` instance of the temporary file 64 | 65 | ### ExcelSheet 66 | 67 | Property | Description 68 | ---------|------------- 69 | `data` | An array of data rows that should be used as sheet content 70 | `titles` (optional) | An array of column titles 71 | `types` (optional) | An array of types for specific columns as supported by PHPOffice, e.g. `DataType::TYPE_STRING`, indexed either by column name (e.g. `H`) or 0-based column index. 72 | `formats` (optional) | An array of format strings for specific columns as supported by Excel, e.g. `#,##0.00`, indexed either by column name (e.g. `H`) or 0-based column index. 73 | `formatters` (optional) | An array of value formatters for specific columns. Each must be a valid PHP callable whith the signature `function formatter($value, $row, $data)` where `$value` is the cell value to format, `$row` is the 0-based row index and `$data` is the current row data from the `data` configuration. The callbacks must be indexed either by column name (e.g. `H`) or by the 0-based column index. 74 | `styles` (optional) | An array of style configuration indexed by cell coordinates or a range. 75 | `callbacks` (optional) | An array of callbacks indexed by column that should be called after rendering a cell, e.g. to apply further complex styling. Each must be a valid PHP callable with the signature `function callback($cell, $col, $row)` where `$cell` is the current `PhpOffice\PhpSpreadsheet\Cell\Cell` object and `$col` and `$row` are the 0-based column and row indices respectively. 76 | `startColumn` (optional) | The start column name or its 0-based index. When this is set, the 0-based offset is added to all numeric keys used anywhere in this class. Columns referenced by name will stay unchanged. Default is 'A'. 77 | `startRow` (optional) | The start row. Default is 1. 78 | 79 | 80 | Event | Description 81 | ---------|------------- 82 | `beforeRender` | Triggered before the sheet is rendered. The sheet is available via `$event->sender->getSheet()`. 83 | `afterRender` | Triggered after the sheet was rendered. The sheet is available via `$event->sender->getSheet()`. 84 | 85 | 86 | ### ActiveExcelSheet 87 | 88 | The class extends from `ExcelSheet` but differs in the following properties: 89 | 90 | Property | Description 91 | ---------|------------- 92 | `query` | The `ActiveQuery` for the row data (the `data` property will be ignored). 93 | `data` | The read-only property that returns the batched query result. 94 | `attributes` (optional) | The attributes to use as columns. Related attributes can be specifed in dot notation as usual, e.g. `team.name`. If not set, the `attributes()` from the corresponding `ActiveRecord` class will be used. 95 | `titles` (optional) | The column titles, indexed by column name (e.g. `H`) or 0-based column index. If a column is not listed here, the respective attribute label will be used. If set to `false` no title row will be rendered. 96 | `formats` (optional) | As in `ExcelSheet` but for `date`, `datetime` and `decimal` DB columns, the respective formats will be automatically set by default, according to the respective date format properties (see below) and the decimal precision. 97 | `formatters` (optional) | As in `ExcelSheet` but for `date` and `datetime` columns the value will be autoconverted to the correct excel time format with `\PHPExcel_Shared_Date::PHPToExcel()` by default. 98 | `dateFormat` | The excel format to use for `date` DB types. Default is `dd/mm/yyyy`. 99 | `dateTimeFormat` | The excel format to use for `datetime` DB types. Default is `dd/mm/yyyy hh:mm:ss`. 100 | `batchSize` | The query batchsize to use. Default is `100`. 101 | `modelInstance` (optional) | The query's `modelClass` instance used to obtain attribute types and titles. If not set an instance of the query's `modelClass` is created automatically. 102 | 103 | > **Note** Since version 2.3.1 datetime attributes will automatically be 104 | > converted to the correct timezone. This feature makes use of the current 105 | > [defaultTimeZone](http://www.yiiframework.com/doc-2.0/yii-i18n-formatter.html#$defaultTimeZone-detail) 106 | > and 107 | > [timeZone](http://www.yiiframework.com/doc-2.0/yii-base-application.html#getTimeZone()-detail) 108 | > setting of the app. 109 | 110 | ## Examples 111 | 112 | ### ActiveQuery results 113 | 114 | ```php 115 | 'codemix\excelexport\ExcelFile', 118 | 119 | 'writerClass' => '\PhpOffice\PhpSpreadsheet\Writer\Xls', // Override default of `\PhpOffice\PhpSpreadsheet\Writer\Xlsx` 120 | 121 | 'sheets' => [ 122 | 123 | 'Active Users' => [ 124 | 'class' => 'codemix\excelexport\ActiveExcelSheet', 125 | 'query' => User::find()->where(['active' => true]), 126 | 127 | // If not specified, all attributes from `User::attributes()` are used 128 | 'attributes' => [ 129 | 'id', 130 | 'name', 131 | 'email', 132 | 'team.name', // Related attribute 133 | 'created_at', 134 | ], 135 | 136 | // If not specified, the label from the respective record is used. 137 | // You can also override single titles, like here for the above `team.name` 138 | 'titles' => [ 139 | 'D' => 'Team Name', 140 | ], 141 | ], 142 | 143 | ], 144 | ]); 145 | $file->send('demo.xlsx'); 146 | ``` 147 | 148 | ### Raw data 149 | 150 | ```php 151 | 'codemix\excelexport\ExcelFile', 154 | 'sheets' => [ 155 | 156 | 'Result per Country' => [ // Name of the excel sheet 157 | 'data' => [ 158 | ['fr', 'France', 1.234, '2014-02-03 12:13:14'], 159 | ['de', 'Germany', 2.345, '2014-02-05 19:18:39'], 160 | ['uk', 'United Kingdom', 3.456, '2014-03-03 16:09:04'], 161 | ], 162 | 163 | // Set to `false` to suppress the title row 164 | 'titles' => [ 165 | 'Code', 166 | 'Name', 167 | 'Volume', 168 | 'Created At', 169 | ], 170 | 171 | 'formats' => [ 172 | // Either column name or 0-based column index can be used 173 | 'C' => '#,##0.00', 174 | 3 => 'dd/mm/yyyy hh:mm:ss', 175 | ], 176 | 177 | 'formatters' => [ 178 | // Dates and datetimes must be converted to Excel format 179 | 3 => function ($value, $row, $data) { 180 | return \PhpOffice\PhpSpreadsheet\Shared\Date::PHPToExcel(strtotime($value)); 181 | }, 182 | ], 183 | ], 184 | 185 | 'Countries' => [ 186 | // Data for another sheet goes here ... 187 | ], 188 | ] 189 | ]); 190 | // Save on disk 191 | $file->saveAs('/tmp/export.xlsx'); 192 | ``` 193 | 194 | ### Query builder results 195 | 196 | ```php 197 | 'codemix\excelexport\ExcelFile', 200 | 'sheets' => [ 201 | 202 | 'Users' => [ 203 | 'data' => new (\yii\db\Query) 204 | ->select(['id','name','email']) 205 | ->from('user') 206 | ->each(100); 207 | 'titles' => ['ID', 'Name', 'Email'], 208 | ], 209 | ] 210 | ]); 211 | $file->send('demo.xlsx'); 212 | ``` 213 | 214 | ### Styling 215 | 216 | Since version 2.3.0 you can style single cells and cell ranges via the `styles` 217 | property of a sheet. For details on the accepted styling format please consult the 218 | [PhpSpreadsheet documentation](https://phpoffice.github.io/PhpSpreadsheet/namespaces/phpoffice-phpspreadsheet-style.html). 219 | 220 | ```php 221 | 'codemix\excelexport\ExcelFile', 224 | 'sheets' => [ 225 | 'Users' => [ 226 | 'class' => 'codemix\excelexport\ActiveExcelSheet', 227 | 'query' => User::find(), 228 | 'styles' => [ 229 | 'A1:Z1000' => [ 230 | 'font' => [ 231 | 'bold' => true, 232 | 'color' => ['rgb' => 'FF0000'], 233 | 'size' => 15, 234 | 'name' => 'Verdana' 235 | ], 236 | 'alignment' => [ 237 | 'horizontal' => Alignment::HORIZONTAL_RIGHT, 238 | ], 239 | ], 240 | ], 241 | ] 242 | ] 243 | ]); 244 | ``` 245 | 246 | As you have access to the `PHPExcel` object you can also "manually" modify the excel file as you like. 247 | 248 | 249 | ```php 250 | createSheets(); 253 | $file 254 | ->getWorkbook(); 255 | ->getSheet(1) 256 | ->getStyle('B1') 257 | ->getFont() 258 | ->getColor() 259 | ->setARGB(\PhpOffice\PhpSpreadsheet\Style\Color::COLOR_RED); 260 | $file->send(); 261 | ``` 262 | 263 | Alternatively you can also use the callback feature from our `ExcelSheet`: 264 | 265 | ```php 266 | 'codemix\excelexport\ExcelFile', 269 | 'sheets' => [ 270 | 'Users' => [ 271 | 'class' => 'codemix\excelexport\ActiveExcelSheet', 272 | 'query' => User::find(), 273 | 'callbacks' => [ 274 | // $cell is a \PhpOffice\PhpSpreadsheet\Cell object 275 | 'A' => function ($cell, $row, $column) { 276 | $cell->getStyle()->applyFromArray([ 277 | 'font' => [ 278 | 'bold' => true, 279 | ], 280 | 'alignment' => [ 281 | 'horizontal' => \PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_RIGHT, 282 | ], 283 | 'borders' => [ 284 | 'top' => [ 285 | 'style' => \PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN, 286 | ], 287 | ], 288 | 'fill' => [ 289 | 'fillType' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_GRADIENT_LINEAR, 290 | 'rotation' => 90, 291 | 'startColor' => [ 292 | 'argb' => 'FFA0A0A0', 293 | ], 294 | 'endColor' => [ 295 | 'argb' => 'FFFFFFFF', 296 | ], 297 | ], 298 | ]); 299 | }, 300 | ], 301 | ], 302 | ], 303 | ]); 304 | ``` 305 | 306 | ### Events 307 | 308 | Since version 2.5.0 there are new events which make it easier to further modify each sheet. 309 | 310 | ```php 311 | 'codemix\excelexport\ExcelFile', 314 | 'sheets' => [ 315 | 'Users' => [ 316 | 'class' => 'codemix\excelexport\ActiveExcelSheet', 317 | 'query' => User::find(), 318 | 'startRow' => 3, 319 | 'on beforeRender' => function ($event) { 320 | $sheet = $event->sender->getSheet(); 321 | $sheet->setCellValue('A1', 'List of current users'); 322 | } 323 | ], 324 | ], 325 | ]); 326 | ``` 327 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codemix/yii2-excelexport", 3 | "description": "A utility to quickly create Excel files from query results or raw data", 4 | "keywords": ["yii2", "excel", "export"], 5 | "type": "yii2-extension", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Michael Härtl", 10 | "email": "haertl.mike@gmail.com" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=7.4", 15 | "yiisoft/yii2": "~2.0.13", 16 | "mikehaertl/php-tmpfile": "^1.0.0", 17 | "phpoffice/phpspreadsheet": "^1.25.2" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "codemix\\excelexport\\": "src/" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/ActiveExcelSheet.php: -------------------------------------------------------------------------------- 1 | _query === null) { 42 | throw new \Exception('No query set'); 43 | } 44 | return $this->_query; 45 | } 46 | 47 | /** 48 | * @param yii\db\ActiveQuery $value the query for the sheet data 49 | */ 50 | public function setQuery($value) 51 | { 52 | $this->_query = $value; 53 | } 54 | 55 | /** 56 | * @return \yii\db\BatchQueryResult the row records in batches of `$batchSize` 57 | */ 58 | public function getData() 59 | { 60 | return $this->getQuery()->each($this->batchSize); 61 | } 62 | 63 | /** 64 | * @return string[] 0-based list of attributes for the table columns. If no 65 | * attributes are set, attributes are set to `ActiveRecord::attributes()` 66 | * for the main query record. 67 | */ 68 | public function getAttributes() 69 | { 70 | if ($this->_attributes === null) { 71 | $this->_attributes = $this->getModelInstance()->attributes(); 72 | } 73 | return $this->_attributes; 74 | } 75 | 76 | /** 77 | * @param string[] $value 0-based list of attributes for the table columns 78 | */ 79 | public function setAttributes($value) 80 | { 81 | $this->_attributes = $value; 82 | } 83 | 84 | /** 85 | * @inheritdoc 86 | */ 87 | public function setData($value) 88 | { 89 | throw new \Exception('Data can not be set on ActiveExcelSheet'); 90 | } 91 | 92 | /** 93 | * @return string[] the column titles. If not set, the respective attribute 94 | * label is used 95 | */ 96 | public function getTitles() 97 | { 98 | if ($this->_titles === null) { 99 | $model = $this->getModelInstance(); 100 | $this->_titles = array_map(function ($a) use ($model) { 101 | return $model->getAttributeLabel($a); 102 | }, $this->getAttributes()); 103 | } 104 | return $this->_titles; 105 | } 106 | 107 | /** 108 | * @param string[]|false $value the column titles indexed by 0-based column 109 | * index. The array is merged with the default titles from `getTitles()` 110 | * (=attribute labels). If an empty array or `false`, no titles will be 111 | * generated. 112 | */ 113 | public function setTitles($value) 114 | { 115 | if (!$value) { 116 | $this->_titles = $value; 117 | } else { 118 | if ($this->_titles === null) { 119 | $this->getTitles(); // Sets attribute labels as defaults 120 | } 121 | foreach ($value as $i => $v) { 122 | $this->_titles[$i] = $v; 123 | } 124 | } 125 | } 126 | 127 | /** 128 | * @param string[] $value the format strings for the column cells indexed 129 | * by 0-based column index. If not set, the formats are auto-generated 130 | * from the DB column types. 131 | */ 132 | public function getFormats() 133 | { 134 | if ($this->_formats === null) { 135 | $this->_formats = []; 136 | $attrs = $this->getAttributes(); 137 | $schemas = $this->getColumnSchemas(); 138 | foreach ($attrs as $c => $attr) { 139 | if (!isset($schemas[$c])) { 140 | continue; 141 | } 142 | switch ($schemas[$c]->type) { 143 | case 'date': 144 | $this->_formats[$c] = $this->dateFormat; 145 | break; 146 | case 'datetime': 147 | $this->_formats[$c] = $this->dateTimeFormat; 148 | break; 149 | case 'decimal': 150 | $decimals = str_pad('#,', $schemas[$c]->scale, '#'); 151 | $zeroPad = str_pad('0.', $schemas[$c]->scale, '0'); 152 | $this->_formats[$c] = $decimals.$zeroPad; 153 | break; 154 | } 155 | } 156 | } 157 | return $this->_formats; 158 | } 159 | 160 | /** 161 | * @param string[]|false $value the format strings for the column cells 162 | * indexed by 0-based column index. The array is merged with the default 163 | * formats from `getFormats()` (auto-generated from DB columns). If an 164 | * empty array or `false`, no formats are applied. 165 | */ 166 | public function setFormats($value) 167 | { 168 | if (!$value) { 169 | $this->_formats = $value; 170 | } else { 171 | if ($this->_formats === null) { 172 | $this->getFormats(); // Sets auto-generated formats as defaults 173 | } 174 | foreach ($value as $i => $v) { 175 | $this->_formats[$i] = $v; 176 | } 177 | } 178 | } 179 | 180 | /** 181 | * @return Callable[] the value formatters for the column cells indexed by 182 | * 0-based column index. If not set, the formatters are auto-generated from 183 | * the DB column types. 184 | */ 185 | public function getFormatters() 186 | { 187 | if ($this->_formatters === null) { 188 | $this->_formatters = []; 189 | $attrs = $this->getAttributes(); 190 | $schemas = $this->getColumnSchemas(); 191 | foreach ($attrs as $c => $attr) { 192 | if (!isset($schemas[$c])) { 193 | continue; 194 | } 195 | switch ($schemas[$c]->type) { 196 | case 'date': 197 | $this->_formatters[$c] = function ($v) { 198 | if (empty($v)) { 199 | return null; 200 | } else { 201 | // Set the correct timezone before converting to a UNIX timestamp. 202 | // This prevents dates from being altered due to timezone 203 | // conversion, e.g. 204 | // '2017-12-05 00:00:00' could become 205 | // '2017-12-04 23:00:00' 206 | $timezone = date_default_timezone_get(); 207 | date_default_timezone_set(Yii::$app->formatter->defaultTimeZone); 208 | $timestamp = strtotime($v); 209 | date_default_timezone_set($timezone); 210 | return \PhpOffice\PhpSpreadsheet\Shared\Date::PHPToExcel($timestamp); 211 | } 212 | }; 213 | break; 214 | case 'datetime': 215 | $this->_formatters[$c] = function ($v) { 216 | if (empty($v)) { 217 | return null; 218 | } else { 219 | return \PhpOffice\PhpSpreadsheet\Shared\Date::PHPToExcel($this->toExcelTime($v)); 220 | } 221 | }; 222 | break; 223 | } 224 | } 225 | } 226 | return $this->_formatters; 227 | } 228 | 229 | /** 230 | * @param Callable[]|null $value the value formatters for the column cells 231 | * indexed by 0-based column index. The array is merged with the default 232 | * formats from `getFormatters()` (auto-generated from DB columns). If an 233 | * empty array or `false`, no formatters are applied. 234 | */ 235 | public function setFormatters($value) 236 | { 237 | if (!$value) { 238 | $this->_formatters = $value; 239 | } else { 240 | if ($this->_formatters === null) { 241 | $this->getFormatters(); // Sets auto-generated formatters as defaults 242 | } 243 | foreach ($value as $i => $v) { 244 | $this->_formatters[$i] = $v; 245 | } 246 | } 247 | } 248 | 249 | /** 250 | * @return yii\db\ActiveRecord an instance of the main model on which the 251 | * query is performed on. This is used to obtain column titles and types. 252 | */ 253 | public function getModelInstance() 254 | { 255 | if ($this->_modelInstance === null) { 256 | $class = $this->getQuery()->modelClass; 257 | $this->_modelInstance = new $class; 258 | } 259 | return $this->_modelInstance; 260 | } 261 | 262 | /** 263 | * @param yii\db\ActiveRecord $model an instance of the main model on which 264 | * the query is performed on. This is used to obtain column titles and 265 | * types. 266 | */ 267 | public function setModelInstance($model) 268 | { 269 | $this->_modelInstance = $model; 270 | } 271 | 272 | /** 273 | * @return yii\db\ActiveRecord a new instance of a related model for the 274 | * given model. This is used to obtain column types. 275 | */ 276 | protected static function getRelatedModelInstance($model, $name) 277 | { 278 | $class = $model->getRelation($name)->modelClass; 279 | return new $class; 280 | } 281 | 282 | /** 283 | * @return yii\db\ColumnSchema[] the DB column schemas indexed by 0-based 284 | * column index. This only includes columns for which a DB schema exists. 285 | */ 286 | protected function getColumnSchemas() 287 | { 288 | if ($this->_columnSchemas === null) { 289 | $model = $this->getModelInstance(); 290 | $schemas = array_map(function ($attr) use ($model) { 291 | return self::getSchema($model, $attr); 292 | }, $this->getAttributes()); 293 | // Filter out null values 294 | $this->_columnSchemas = array_filter($schemas); 295 | } 296 | return $this->_columnSchemas; 297 | } 298 | 299 | /** 300 | * @inheritdoc 301 | */ 302 | protected function renderRow($data, $row, $formats, $formatters, $callbacks, $types) 303 | { 304 | $values = array_map(function ($attr) use ($data) { 305 | return ArrayHelper::getValue($data, $attr); 306 | }, $this->getAttributes()); 307 | return parent::renderRow($values, $row, $formats, $formatters, $callbacks, $types); 308 | } 309 | 310 | /** 311 | * Convert a datetime to the right excel timestamp 312 | * 313 | * This method will use [[\yii\i18n\Formatter::defaultTimeZone]] and 314 | * [[\yii\base\Application::timeZone]] to convert the given datetime 315 | * from DB to application timezone. 316 | * 317 | * @param string $value the datetime value 318 | * @return int timezone offset in seconds 319 | * @see [[yii\i18n\Formatter::defaultTimezone]] 320 | * @see [[yii\i18n\Formatter::timezone]] 321 | */ 322 | protected function toExcelTime($value) 323 | { 324 | // "Cached" timezone instances 325 | static $defaultTimezone; 326 | static $timezone; 327 | 328 | if (Yii::$app->formatter->defaultTimeZone === Yii::$app->timeZone) { 329 | return strtotime($value); 330 | } else { 331 | if ($timezone === null) { 332 | $defaultTimezone = new \DateTimeZone(Yii::$app->formatter->defaultTimeZone); 333 | $timezone = new \DateTimeZone(Yii::$app->timeZone); 334 | } 335 | 336 | // Offset can depend on given datetime due to DST 337 | $defaultDatetime = new \DateTime($value, $defaultTimezone); 338 | $offset = $timezone->getOffset($defaultDatetime); 339 | 340 | // PHPExcel_Shared_Date::PHPToExcel() method expects a 341 | // "pseudo-timestamp": Something like a UNIX timestamp but 342 | // including local timezone offset. 343 | return $defaultDatetime->getTimestamp() + $offset; 344 | } 345 | } 346 | 347 | /** 348 | * Returns either the ColumnSchema or a new instance of the related model 349 | * for the given attribute name. 350 | * 351 | * The name can be specified in dot format, like `company.name` in which 352 | * case the ColumnSchema for the `name` attribute in the related `company` 353 | * record would be returned. 354 | * 355 | * If the attribute is a relation name (which could also use dot notation) 356 | * then `$isRelation` must be set to `true`. In this case an instance of 357 | * the related ActiveRecord class is returned. 358 | * 359 | * @param yii\db\ActiveRecord $model the model where the attribute exist 360 | * @param string $attribute name of the attribute 361 | * @param mixed $isRelation whether the name specifies a relation, in which 362 | * case an `ActiveRecord` is returned. Default is `false`, which returns a 363 | * `ColumnSchema`. 364 | * @return yii\db\ColumnSchema|yii\db\ActiveRecord|null the type instance 365 | * of the attribute or `null` if the attribute is not a DB column (e.g. 366 | * public property or defined by getter) 367 | */ 368 | public static function getSchema($model, $attribute, $isRelation = false) 369 | { 370 | if (($pos = strrpos($attribute, '.')) !== false) { 371 | $model = self::getSchema($model, substr($attribute, 0, $pos), true); 372 | $attribute = substr($attribute, $pos + 1); 373 | } 374 | if ($isRelation) { 375 | return self::getRelatedModelInstance($model, $attribute); 376 | } else { 377 | $columnSchemas = $model->getTableSchema()->columns; 378 | return isset($columnSchemas[$attribute]) ? $columnSchemas[$attribute] : null; 379 | } 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /src/ExcelFile.php: -------------------------------------------------------------------------------- 1 | _writer === null) { 41 | $class = $this->writerClass; 42 | $this->_writer = new $class($this->getWorkbook()); 43 | } 44 | return $this->_writer; 45 | } 46 | 47 | /** 48 | * @return \PhpOffice\PhpSpreadsheet\Spreadsheet the workbook instance 49 | */ 50 | public function getWorkbook() 51 | { 52 | if ($this->_workbook === null) { 53 | $this->_workbook = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); 54 | } 55 | return $this->_workbook; 56 | } 57 | 58 | /** 59 | * @return mikehaertl\tmp\File the instance of the temporary excel file 60 | */ 61 | public function getTmpFile() 62 | { 63 | if ($this->_tmpFile === null) { 64 | $suffix = ArrayHelper::getValue($this->fileOptions, 'suffix'); 65 | $prefix = ArrayHelper::getValue($this->fileOptions, 'prefix'); 66 | $directory = ArrayHelper::getValue($this->fileOptions, 'directory'); 67 | $this->_tmpFile = new File('', $suffix, $prefix, $directory); 68 | } 69 | return $this->_tmpFile; 70 | } 71 | 72 | /** 73 | * @return array the sheet configuration 74 | */ 75 | public function getSheets() 76 | { 77 | return $this->_sheets; 78 | } 79 | 80 | /** 81 | * @param array $value the sheet configuration. This must be an array where 82 | * keys are sheet names and values are arrays with the configuration 83 | * options for an instance if `ExcelSheet`. 84 | */ 85 | public function setSheets($value) 86 | { 87 | $this->_sheets = $value; 88 | } 89 | 90 | /** 91 | * Save the file under the given name 92 | * 93 | * @param string $filename 94 | * @return bool whether the file was saved successfully 95 | */ 96 | public function saveAs($filename) 97 | { 98 | $this->createFile(); 99 | return $this->getTmpFile()->saveAs($filename); 100 | } 101 | 102 | /** 103 | * Send the Excel file for download 104 | * 105 | * @param string|null $filename the filename to send. If empty, the file is 106 | * streamed inline. 107 | * @param bool $inline whether to force inline display of the file, even if 108 | * filename is present. 109 | * @param string $contentType the Content-Type header. Default is 110 | * 'application/vnd.ms-excel'. 111 | */ 112 | public function send($filename = null, $inline = false, $contentType = 'application/vnd.ms-excel') 113 | { 114 | $this->createFile(); 115 | $this->getTmpFile()->send($filename, $contentType, $inline); 116 | } 117 | 118 | /** 119 | * Create the Excel sheets if they were not created yet 120 | */ 121 | public function createSheets() 122 | { 123 | if (!$this->_sheetsCreated) { 124 | $workbook = $this->getWorkbook(); 125 | $i = 0; 126 | foreach ($this->sheets as $title => $config) { 127 | if (is_string($config)) { 128 | $config = ['class' => $config]; 129 | } elseif (is_array($config)) { 130 | if (!isset($config['class'])) { 131 | $config['class'] = ExcelSheet::className(); 132 | } 133 | } elseif (!is_object($config)) { 134 | throw new \Exception('Invalid sheet configuration'); 135 | } 136 | $sheet = ($i === 0) ? 137 | $workbook->getActiveSheet() : $workbook->createSheet(); 138 | if (is_string($title)) { 139 | $sheet->setTitle($title); 140 | } 141 | Yii::createObject($config, [$sheet])->render(); 142 | $i++; 143 | } 144 | $this->_sheetsCreated = true; 145 | } 146 | } 147 | 148 | /** 149 | * Create the Excel file and save it to the temp file 150 | */ 151 | protected function createFile() 152 | { 153 | if (!$this->_fileCreated) { 154 | $this->createSheets(); 155 | $this->getWriter()->save((string) $this->getTmpFile()); 156 | $this->_fileCreated = true; 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/ExcelSheet.php: -------------------------------------------------------------------------------- 1 | _sheet = $sheet; 48 | } 49 | 50 | /** 51 | * @return \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet 52 | */ 53 | public function getSheet() 54 | { 55 | return $this->_sheet; 56 | } 57 | 58 | /** 59 | * @return array|\Iterator the data for the rows of the sheet 60 | */ 61 | public function getData() 62 | { 63 | return $this->_data; 64 | } 65 | 66 | /** 67 | * @param array|\Iterator $value the data for the rows of the sheet 68 | */ 69 | public function setData($value) 70 | { 71 | $this->_data = $value; 72 | } 73 | 74 | /** 75 | * @return string[]|null|false the column titles indexed by column name or 76 | * 0-based index. If empty, `null` or `false`, no titles will be generated. 77 | */ 78 | public function getTitles() 79 | { 80 | return $this->_titles; 81 | } 82 | 83 | /** 84 | * @param string[]|null|false $value the column titles indexed by 0-based 85 | * column index. If empty or `false`, no titles will be generated. 86 | */ 87 | public function setTitles($value) 88 | { 89 | $this->_titles = $value; 90 | } 91 | 92 | /** 93 | * @return string[]|null the types for the column cells indexed by 0-based 94 | * column index. See the `PHPExcel_Cell_DataType::TYPE_*` constants for 95 | * available types. If no type is set for a column, PHPExcel will 96 | * autodetect the correct type. 97 | */ 98 | public function getTypes() 99 | { 100 | return $this->_types; 101 | } 102 | 103 | /** 104 | * @param string[]|null $value the types for the column cells indexed by 105 | * 0-based column index 106 | */ 107 | public function setTypes($value) 108 | { 109 | $this->_types = $value; 110 | } 111 | 112 | /** 113 | * @return string[]|null the format strings for the column cells indexed by 114 | * 0-based column index 115 | */ 116 | public function getFormats() 117 | { 118 | return $this->_formats; 119 | } 120 | 121 | /** 122 | * @param string[]|null $value the format strings for the column cells 123 | * indexed by 0-based column index 124 | */ 125 | public function setFormats($value) 126 | { 127 | $this->_formats = $value; 128 | } 129 | 130 | /** 131 | * @return Callable[]|null the value formatters for the column cells 132 | * indexed by 0-based column index. The function signature is `function 133 | * ($value, $row, $data)` where `$value` is the cell value, `$row` is the 134 | * row index and `$data` is the row data. 135 | */ 136 | public function getFormatters() 137 | { 138 | return $this->_formatters; 139 | } 140 | 141 | /** 142 | * @param Callable[]|null $value the value formatters for the column cells 143 | * indexed by 0-based column index 144 | */ 145 | public function setFormatters($value) 146 | { 147 | $this->_formatters = $value; 148 | } 149 | 150 | /** 151 | * @return array style configuration arrays indexed by cell coordinate or 152 | * cell range, e.g. `A1:Z1000`. 153 | */ 154 | public function getStyles() 155 | { 156 | return $this->_styles; 157 | } 158 | 159 | /** 160 | * @param array $value style configuration arrays indexed by cell 161 | * coordinate or cell range, e.g. `A1:Z1000`. 162 | */ 163 | public function setStyles($value) 164 | { 165 | $this->_styles = $value; 166 | } 167 | 168 | /** 169 | * @return Callable[]|null column callbacks indexed by 0-based column index 170 | * that get called after rendering a cell. The function signature is 171 | * `function ($cell, $column, $row)` where `$cell` is the `PHPExcel_Cell` 172 | * object and `$row` and `$column` are the row and column index. 173 | */ 174 | public function getCallbacks() 175 | { 176 | return $this->_callbacks; 177 | } 178 | 179 | /** 180 | * @param Callable[]|null $value callbacks that get called after rendering 181 | * a column cell indexed by 0-based column index. 182 | */ 183 | public function setCallbacks($value) 184 | { 185 | $this->_callbacks = $value; 186 | } 187 | 188 | /** 189 | * Render the sheet 190 | */ 191 | public function render() 192 | { 193 | $this->beforeRender(); 194 | $this->_row = $this->startRow; 195 | $this->renderStyles(); 196 | $this->renderTitle(); 197 | $this->renderRows(); 198 | $this->trigger(self::EVENT_AFTER_RENDER); 199 | } 200 | 201 | /** 202 | * Trigger the [[EVENT_BEFORE_RENDER]] event 203 | */ 204 | public function beforeRender() 205 | { 206 | $this->trigger(self::EVENT_BEFORE_RENDER); 207 | } 208 | 209 | /** 210 | * Trigger the [[EVENT_AFTER_RENDER]] event 211 | */ 212 | public function afterRender() 213 | { 214 | $this->trigger(self::EVENT_AFTER_RENDER); 215 | } 216 | 217 | /** 218 | * Render styles 219 | */ 220 | protected function renderStyles() 221 | { 222 | foreach ($this->getStyles() as $i => $style) { 223 | $this->_sheet->getStyle($i)->applyFromArray($style); 224 | } 225 | } 226 | 227 | /** 228 | * Render the title row if any 229 | */ 230 | protected function renderTitle() 231 | { 232 | $titles = $this->normalizeIndex($this->getTitles()); 233 | if ($titles) { 234 | $keys = array_keys($titles); 235 | $col = array_shift($keys); 236 | foreach ($titles as $title) { 237 | $this->_sheet->setCellValueByColumnAndRow($col++, $this->_row, $title); 238 | } 239 | $this->_row++; 240 | } 241 | } 242 | 243 | /** 244 | * Render the data rows if any 245 | */ 246 | protected function renderRows() 247 | { 248 | $formats = $this->normalizeIndex($this->getFormats()); 249 | $formatters = $this->normalizeIndex($this->getFormatters()); 250 | $callbacks = $this->normalizeIndex($this->getCallbacks()); 251 | $types = $this->normalizeIndex($this->getTypes()); 252 | 253 | foreach ($this->getData() as $data) { 254 | $this->renderRow($data, $this->_row++, $formats, $formatters, $callbacks, $types); 255 | } 256 | } 257 | 258 | /** 259 | * Render a single row 260 | * 261 | * @param array $data the row data 262 | * @param int $row the index of the current row 263 | * @param mixed $formats formats with normalized index 264 | * @param mixed $formatters formatters with normalized index 265 | * @param mixed $callbacks callbacks with normalized index 266 | * @param mixed $types types with normalized index 267 | */ 268 | protected function renderRow($data, $row, $formats, $formatters, $callbacks, $types) 269 | { 270 | foreach (array_values($data) as $i => $value) { 271 | $col = $i + self::normalizeColumn($this->startColumn); 272 | if (isset($formatters[$col]) && is_callable($formatters[$col])) { 273 | $value = call_user_func($formatters[$col], $value, $row, $data); 274 | } 275 | if (isset($types[$col])) { 276 | $this->_sheet->setCellValueExplicitByColumnAndRow($col, $row, $value, $types[$col]); 277 | } else { 278 | $this->_sheet->setCellValueByColumnAndRow($col, $row, $value); 279 | } 280 | if (isset($formats[$col])) { 281 | $this->_sheet 282 | ->getStyleByColumnAndRow($col, $row) 283 | ->getNumberFormat() 284 | ->setFormatCode($formats[$col]); 285 | } 286 | if (isset($callbacks[$col]) && is_callable($callbacks[$col])) { 287 | $cell = $this->_sheet->getCellByColumnAndRow($col, $row); 288 | call_user_func($callbacks[$col], $cell, $col, $row); 289 | } 290 | } 291 | } 292 | 293 | /** 294 | * @param array $data any data indexed by 0-based colum index or by column name. 295 | * @return array the array with alphanumeric column keys (A, B, C, ...) 296 | * converted to numeric indices and numeric indices converted to 1-based 297 | * and startColumn added 298 | */ 299 | protected function normalizeIndex($data) 300 | { 301 | if (!is_array($data)) { 302 | return $data; 303 | } 304 | $result = []; 305 | foreach ($data as $k => $v) { 306 | $result[self::normalizeColumn($k)] = $v; 307 | } 308 | return $result; 309 | } 310 | 311 | /** 312 | * @param int|string $column the column either as 0-based index or as string. If 313 | * numeric, the startColumn offset will be added. 314 | * @return int the normalized numeric column index (1-based). 315 | */ 316 | public function normalizeColumn($column) 317 | { 318 | if (is_string($column)) { 319 | return \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($column); 320 | } else { 321 | $startIndex = is_string($this->startColumn) ? 322 | self::normalizeColumn($this->startColumn) : 323 | ($this->startColumn + 1); 324 | return $column + $startIndex; 325 | } 326 | } 327 | } 328 | --------------------------------------------------------------------------------