├── .gitignore ├── README.md ├── composer.json └── src ├── Bootstrap.php ├── ExcelReport.php ├── ExcelReportHelper.php ├── ExcelReportModel.php ├── ExcelReportQueue.php ├── Module.php ├── ProgressBehavior.php ├── controllers └── ReportController.php ├── messages ├── config.php └── ru │ └── customit.php └── views ├── _form.php └── _progress.php /.gitignore: -------------------------------------------------------------------------------- 1 | # yii console commands 2 | /yii 3 | /yii_test 4 | /yii_test.bat 5 | 6 | # phpstorm project files 7 | .idea 8 | 9 | # netbeans project files 10 | nbproject 11 | 12 | # zend studio for eclipse project files 13 | .buildpath 14 | .project 15 | .settings 16 | 17 | # windows thumbnail cache 18 | Thumbs.db 19 | 20 | # composer vendor dir 21 | /vendor 22 | 23 | # composer itself is not needed 24 | composer.phar 25 | 26 | # Mac DS_Store Files 27 | .DS_Store 28 | 29 | # phpunit itself is not needed 30 | phpunit.phar 31 | # local phpunit config 32 | /phpunit.xml 33 | 34 | # vagrant runtime 35 | /.vagrant 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | :no_entry: 2 | ```diff 3 | - !!! Library is deprecated !!! 4 | ``` 5 | :no_entry: 6 | 7 | 8 |

9 | 10 | 11 | 12 |

Yii2 ExcelReport Extension

13 |
14 |

15 | 16 | 17 | An extension for generate excel file from GridView content. When used with a GridView, extention saves the results of filtering and sorting in a file. Everything you see in the GridView will be imported into a file. All tasks are run in the background, the user can check the progress with the progressbar. It is not necessary to remain on the current page during the execution. You can continue working with the application. When the file is created, the download link will remain on the page with the widget until it is used, the user can use it at any time. When the file is downloaded, you can start generating a new report. 18 | 19 | **To run tasks in the background, the extension uses a [queues](https://github.com/yiisoft/yii2-queue).** 20 | 21 | Use the extension only makes sense to generate large files (> 50,000 lines). 22 | 23 | [![Latest Stable Version](https://poser.pugx.org/custom-it/yii2-excel-report/v/stable.svg)](https://packagist.org/packages/custom-it/yii2-excel-report) 24 | [![Total Downloads](https://poser.pugx.org/custom-it/yii2-excel-report/downloads.svg)](https://packagist.org/packages/custom-it/yii2-excel-report) 25 | 26 | Installation 27 | ------------ 28 | 29 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 30 | 31 | Either run 32 | 33 | ``` 34 | php composer require --prefer-dist custom-it/yii2-excel-report 35 | ``` 36 | 37 | or add 38 | 39 | ``` 40 | "custom-it/yii2-excel-report": "*" 41 | ``` 42 | 43 | to the require section of your `composer.json` file. 44 | 45 | 46 | Configuration 47 | ------------- 48 | Before using the module, configure the [queues](https://github.com/yiisoft/yii2-queue/blob/master/docs/guide/README.md) 49 | 50 | Add progress behavior to Queue configuration: 51 | ```php 52 | 'queue' => [ 53 | // ... you Queue configuration ... 54 | 'as progress' => \customit\excelreport\ProgressBehavior::class, 55 | ], 56 | ``` 57 | 58 | Usage 59 | ----- 60 | 61 | Once the extension is installed, simply use it in your code by : 62 | 63 | ```php 64 | $gridColumns = [ 65 | ['class' => 'yii\grid\SerialColumn'], 66 | 'id', 67 | 'name', 68 | 'date', 69 | 'post', 70 | ['class' => 'yii\grid\ActionColumn'], 71 | ]; 72 | 73 | // Render widget 74 | echo \customit\excelreport\ExcelReport::widget([ 75 | 'columns' => $gridColumns, 76 | 'dataProvider' => $dataProvider, 77 | ]); 78 | 79 | // Can be used with or without a GridView 80 | echo GridView::widget([ 81 | 'dataProvider' => $dataProvider, 82 | 'filterModel' => $searchModel, 83 | 'columns' => $gridColumns 84 | ]); 85 | ``` 86 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "custom-it/yii2-excel-report", 3 | "description": "An extension for generate excel file from GridView content", 4 | "type": "yii2-extension", 5 | "keywords": ["yii2","extension","excel","report","excelreport"], 6 | "license": "BSD-3-Clause", 7 | "authors": [ 8 | { 9 | "name": "Zodiac163", 10 | "email": "vm@custom-it.ru" 11 | } 12 | ], 13 | "require": { 14 | "yiisoft/yii2": "~2.0.0", 15 | "yiisoft/yii2-queue": "^2.1", 16 | "box/spout": "^2.7", 17 | "jeremeamia/superclosure":"*" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "customit\\excelreport\\": "src" 22 | } 23 | }, 24 | "extra": { 25 | "bootstrap": "customit\\excelreport\\Bootstrap" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Bootstrap.php: -------------------------------------------------------------------------------- 1 | setModule('excelreport', 'customit\excelreport\Module'); 11 | Yii::$app->getModule('excelreport')->registerTranslations(); 12 | } 13 | } -------------------------------------------------------------------------------- /src/ExcelReport.php: -------------------------------------------------------------------------------- 1 | formOptions = ['id' => $this->id,]; 22 | } 23 | 24 | public function run() 25 | { 26 | if (isset($_POST['excelReportAction']) || Yii::$app->session->has('excel-report-progress')) { 27 | if (Yii::$app->session->has('excel-report-progress')) { 28 | $data = unserialize(Yii::$app->session->get('excel-report-progress')); 29 | $fileName = $data['fileName']; 30 | $id = $data['queueid']; 31 | } else { 32 | $this->columns = base64_encode(serialize(ExcelReportHelper::closureDetect($this->columns))); 33 | $this->dataProvider = base64_encode(serialize(ExcelReportHelper::closureDetect($this->dataProvider))); 34 | $fileName = Yii::$app->security->generateRandomString(); 35 | $id = Yii::$app->queue->push(new ExcelReportQueue([ 36 | 'columns' => $this->columns, 37 | 'stripHtml' => $this->stripHtml, 38 | 'fileName' => $fileName, 39 | 'dataProvider' => $this->dataProvider, 40 | ])); 41 | 42 | Yii::$app->session->set('excel-report-progress', serialize(['fileName' => $fileName, 'queueid' => $id])); 43 | } 44 | 45 | return $this->render('_progress', [ 46 | 'options' => $this->formOptions, 47 | 'queueId' => $id, 48 | 'file' => Yii::$app->basePath . '/runtime/export/' . $fileName . '.xlsx', 49 | ]); 50 | } else { 51 | return $this->render('_form', ['options' => $this->formOptions]); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/ExcelReportHelper.php: -------------------------------------------------------------------------------- 1 | &$value) { 13 | if (is_array($value)) { 14 | $value = self::closureDetect($value); 15 | } elseif (is_object($value) && self::is_closure($value)) { 16 | $value = $serializer->serialize($value); 17 | } 18 | } 19 | 20 | return $arr; 21 | } 22 | 23 | public static function reverseClosureDetect($arr) { 24 | $serializer = new Serializer(); 25 | foreach ($arr as $key=>&$value) { 26 | if (is_array($value)) { 27 | $value = self::reverseClosureDetect($value); 28 | } elseif (is_string($value) && strpos($value, "SuperClosure")) { 29 | $value = $serializer->unserialize($value); 30 | } 31 | } 32 | 33 | return $arr; 34 | } 35 | 36 | public static function is_closure($t) { 37 | return $t instanceof \Closure; 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/ExcelReportModel.php: -------------------------------------------------------------------------------- 1 | _provider = ExcelReportHelper::reverseClosureDetect(unserialize(base64_decode($dataProvider))); 100 | $this->_columns = $this->cleanColumns(ExcelReportHelper::reverseClosureDetect(unserialize(base64_decode($columns)))); 101 | $this->queue = $queue; 102 | $this->filename = $fileName; 103 | } 104 | 105 | /** 106 | * Remove extra columns 107 | * @param array $columns array of gridview columns 108 | * @return array 109 | */ 110 | public function cleanColumns($columns) { 111 | foreach ($columns as $key => &$column) { 112 | if (!empty($column['hiddenFromExport'])) { 113 | unset($columns[$key]); 114 | continue; 115 | } 116 | 117 | if (isset($column['class']) && $column['class'] == 'yii\\grid\\ActionColumn') { 118 | unset($columns[$key]); 119 | } elseif (isset($column['class']) && $column['class'] == 'yii\\grid\\SerialColumn') { 120 | unset($columns[$key]); 121 | } elseif (isset($column['value'])) { 122 | $column['attribute'] = $column['value']; 123 | } 124 | 125 | } 126 | return $columns; 127 | } 128 | 129 | /** 130 | * Entry point 131 | */ 132 | public function start() { 133 | $config = [ 134 | 'extension' => 'xlsx', 135 | 'writer' => Type::XLSX, 136 | ]; 137 | $this->initExport(); 138 | try { 139 | $this->initExcelWriter($config); 140 | } catch (InvalidConfigException $e) { 141 | Yii::error($e->getMessage()); 142 | exit; 143 | } catch (IOException $e) { 144 | Yii::error($e->getMessage()); 145 | exit; 146 | } catch (UnsupportedTypeException $e) { 147 | Yii::error($e->getMessage()); 148 | exit; 149 | } 150 | $this->initExcelWorksheet(); 151 | $this->generateHeader(); 152 | $totalCount = $this->generateBody(); 153 | //Write data to file 154 | $this->_objWriter->close(); 155 | $this->queue->setProgress($this->_endRow, $totalCount); 156 | //Unset vars 157 | $this->cleanup(); 158 | } 159 | 160 | /** 161 | * Initializes export settings 162 | */ 163 | public function initExport() 164 | { 165 | $this->setDefaultStyles('header'); 166 | $this->setDefaultStyles('box'); 167 | 168 | if (!isset($this->filename)) { 169 | $this->filename = 'grid-export'; 170 | } 171 | } 172 | 173 | /** 174 | * Appends slash to path if it does not exist 175 | * 176 | * @param string $path 177 | * @param string $s the path separator 178 | * 179 | * @return string 180 | */ 181 | public static function slash($path, $s = DIRECTORY_SEPARATOR) 182 | { 183 | $path = trim($path); 184 | if (substr($path, -1) !== $s) { 185 | $path .= $s; 186 | } 187 | return $path; 188 | } 189 | 190 | /** 191 | * Sets default styles 192 | * 193 | * @param string $section 194 | */ 195 | protected function setDefaultStyles($section) 196 | { 197 | $defaultStyle = []; 198 | $opts = ''; 199 | if ($section === 'header') { 200 | $opts = 'headerStyleOptions'; 201 | 202 | $border = (new BorderBuilder()) 203 | ->setBorderBottom(Color::BLACK, Border::WIDTH_MEDIUM, Border::STYLE_SOLID) 204 | ->build(); 205 | $defaultStyle = (new StyleBuilder()) 206 | ->setFontBold() 207 | ->setBackgroundColor('FFE5E5E5') 208 | ->setBorder($border) 209 | ->build(); 210 | 211 | } elseif ($section === 'box') { 212 | $opts = 'boxStyleOptions'; 213 | 214 | $border = (new BorderBuilder()) 215 | ->setBorderBottom(Color::BLACK, Border::WIDTH_MEDIUM, Border::STYLE_SOLID) 216 | ->build(); 217 | $defaultStyle = (new StyleBuilder()) 218 | ->setBorder($border) 219 | ->build(); 220 | } 221 | if (empty($opts)) { 222 | return; 223 | } 224 | 225 | $this->$opts = $defaultStyle; 226 | } 227 | 228 | /** 229 | * Initializes Spout Writer Object Instance 230 | * 231 | * @param array $config 232 | * @throws InvalidConfigException 233 | * @throws \Box\Spout\Common\Exception\UnsupportedTypeException 234 | * @throws \Box\Spout\Common\Exception\IOException 235 | */ 236 | public function initExcelWriter($config) 237 | { 238 | $this->folder = trim(Yii::getAlias($this->folder)); 239 | $file = self::slash($this->folder) . $this->filename . '.' . $config['extension']; 240 | if (!file_exists($this->folder) && !mkdir($this->folder, 0777, true)) { 241 | throw new InvalidConfigException( 242 | "Invalid permissions to write to '{$this->folder}' as set in `Export::folder` property." 243 | ); 244 | } 245 | 246 | $this->_objWriter = WriterFactory::create($config['writer']); 247 | $this->_objWriter->setShouldUseInlineStrings(true); 248 | $this->_objWriter->setShouldUseInlineStrings(false); 249 | 250 | $this->_objWriter->openToFile($file); 251 | } 252 | 253 | /** 254 | * Get Worksheet Instance 255 | */ 256 | public function initExcelWorksheet() 257 | { 258 | $this->_objWorksheet = $this->_objWriter->getCurrentSheet(); 259 | $this->_objWorksheet->setName(Yii::t('customit','Report')); 260 | } 261 | 262 | /** 263 | * Generates the output data header content. 264 | */ 265 | public function generateHeader() 266 | { 267 | if (count($this->_columns) == 0) { 268 | return; 269 | } 270 | $styleOpts = $this->headerStyleOptions; 271 | $headValues = []; 272 | $this->_endCol = 0; 273 | //Generate labels array 274 | foreach ($this->_columns as $column) { 275 | $this->_endCol++; 276 | if (isset($column['label'])) { 277 | $head = $column['label']; 278 | } elseif (isset($column['header'])) { 279 | $head = $column['header']; 280 | } else { 281 | $head = '#'; 282 | } 283 | $headValues[] = $head; 284 | } 285 | //Write header content 286 | $this->setRowValues($headValues, $styleOpts); 287 | } 288 | 289 | /** 290 | * Sets the values of excel row 291 | * 292 | * @param array $values 293 | * @param array $style 294 | * 295 | */ 296 | protected function setRowValues($values, $style = null) 297 | { 298 | if ($this->stripHtml) { 299 | array_walk_recursive($values, function (&$item, $key) { 300 | $item = strip_tags($item); 301 | }); 302 | } 303 | if (!empty($style)) { 304 | $this->_objWriter->addRowWithStyle($values, $style); 305 | } else { 306 | $this->_objWriter->addRows($values); 307 | } 308 | } 309 | 310 | /** 311 | * Generates the output data body content. 312 | * 313 | * @return integer the number of output rows. 314 | */ 315 | public function generateBody() 316 | { 317 | $this->_endRow = 0; 318 | $totalCount = $this->_provider->getTotalCount(); 319 | 320 | foreach ($this->_provider->query->each() as $value) { 321 | $this->generateRow($value); 322 | $this->_endRow++; 323 | //Change queue process progress 324 | if (($this->_endRow % 1000) == 0) $this->queue->setProgress($this->_endRow-1, $totalCount); 325 | } 326 | 327 | $this->setRowValues($this->_bodyData); 328 | return $totalCount; 329 | } 330 | 331 | /** 332 | * Generates an output data row with the given data. 333 | * 334 | * @param mixed $data the data model to be rendered 335 | */ 336 | public function generateRow($data) 337 | { 338 | $this->_endCol = 0; 339 | $key = count($this->_bodyData); 340 | 341 | foreach ($this->_columns as $column) { 342 | $var = isset($column['attribute']) ? $column['attribute'] : null; 343 | if (is_string($var)) { 344 | $valueChain = explode('.', $var); 345 | $bufObj = $data; 346 | if (count($valueChain) > 1) { 347 | foreach ($valueChain as $vc) { 348 | $bufObj = is_object($bufObj) ? $bufObj->$vc : "---"; 349 | } 350 | $value = $bufObj; 351 | } else { 352 | $value = is_object($data) ? $data->$var : "---"; 353 | } 354 | } elseif (is_object($var) && ExcelReportHelper::is_closure($var)) { 355 | $value = call_user_func($var, $data); 356 | } else { 357 | $value = null; 358 | } 359 | $this->_bodyData[$key][] = isset($column['format']) ? Yii::$app->formatter->format($value, $column['format']) : $value; 360 | } 361 | } 362 | 363 | /** 364 | * Cleans up current objects instance 365 | */ 366 | protected function cleanup() 367 | { 368 | unset($this->_provider, $this->_objWriter); 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /src/ExcelReportQueue.php: -------------------------------------------------------------------------------- 1 | columns, 18 | $queue, 19 | $this->fileName, 20 | $this->dataProvider 21 | ); 22 | $m->start(); 23 | } 24 | 25 | public function getTtr() 26 | { 27 | return 5 * 60; 28 | } 29 | 30 | public function canRetry($attempt, $error) 31 | { 32 | return false; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Module.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright Copyright © Zodiac163, custom-it.ru 7 | * @version 0.0.1 8 | */ 9 | 10 | namespace customit\excelreport; 11 | 12 | class Module extends \yii\base\Module 13 | { 14 | public function init() 15 | { 16 | parent::init(); 17 | $this->registerTranslations(); 18 | } 19 | 20 | public function registerTranslations() 21 | { 22 | \Yii::$app->i18n->translations['customit'] = [ 23 | 'class' => 'yii\i18n\PhpMessageSource', 24 | 'sourceLanguage' => 'en-US', 25 | 'basePath' => __DIR__ . '/messages', 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/ProgressBehavior.php: -------------------------------------------------------------------------------- 1 | function (ExecEvent $event) { 18 | $this->jobId = $event->id; 19 | } 20 | ]; 21 | } 22 | 23 | public function setProgress($pos, $len) 24 | { 25 | $key = __CLASS__ . $this->jobId; 26 | Yii::$app->cache->set($key, [$pos, $len]); 27 | } 28 | 29 | public function getProgress($jobId) 30 | { 31 | $key = __CLASS__ . $jobId; 32 | return Yii::$app->cache->get($key) ?: [0, 1]; 33 | } 34 | 35 | public function setManuallyProgress($jobId, $pos, $len) { 36 | $key = __CLASS__ . $jobId; 37 | Yii::$app->cache->set($key, [$pos, $len]); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/controllers/ReportController.php: -------------------------------------------------------------------------------- 1 | response->format = \yii\web\Response::FORMAT_JSON; 13 | $jobId = $_POST['id']; 14 | $data = []; 15 | if (Yii::$app->session->has('excel-report-progress')){ 16 | $data = unserialize(Yii::$app->session->get('excel-report-progress')); 17 | } 18 | return [ 19 | 'progress' => Yii::$app->queue->getProgress($jobId), 20 | 'info' => $data, 21 | ]; 22 | } 23 | 24 | public function actionDownload() { 25 | if (Yii::$app->session->has('excel-report-progress')){ 26 | $data = unserialize(Yii::$app->session->get('excel-report-progress')); 27 | $file = Yii::$app->basePath . '/runtime/export/' . $data['fileName'] . '.xlsx'; 28 | if (file_exists($file)) { 29 | if (filesize($file) == 0) { 30 | throw new NotFoundHttpException('Файл заканчивает формирование. Осталось всего несколько секунд... Попробуйте нажать на ссылку еще раз '); 31 | return false; 32 | } else { 33 | Yii::$app->session->remove('excel-report-progress'); 34 | ob_clean(); 35 | return \Yii::$app->response->sendFile($file, null, ['mimeType' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']); 36 | } 37 | } else { 38 | Yii::$app->session->remove('excel-report-progress'); 39 | throw new NotFoundHttpException('Такого файла не существует '); 40 | } 41 | 42 | }else{ 43 | throw new NotFoundHttpException('Такого файла не существует '); 44 | } 45 | } 46 | 47 | public function actionReset() { 48 | $jobId = $_POST['id']; 49 | Yii::$app->queue->setManuallyProgress($jobId, 1, 1); 50 | Yii::$app->session->remove('excel-report-progress'); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/messages/config.php: -------------------------------------------------------------------------------- 1 | dirname(__DIR__), 5 | 'messagePath' => __DIR__, 6 | 'languages' => ['ru'], 7 | 'translator' => 'Yii::t', 8 | 'sort' => true, 9 | 'overwrite' => false, 10 | 'removeUnused' => false, 11 | 'markUnused' => true, 12 | 'except' => [ 13 | '.svn', 14 | '.git', 15 | '.gitignore', 16 | '.gitkeep', 17 | '.hgignore', 18 | '.hgkeep', 19 | '/messages', 20 | '/BaseYii.php', 21 | ], 22 | 'only' => ['*.php',], 23 | 'format' => 'php' 24 | ]; -------------------------------------------------------------------------------- /src/messages/ru/customit.php: -------------------------------------------------------------------------------- 1 | 'Сформировать Excel', 21 | 'Download last report' => 'Скачать последний отчет', 22 | 'Report' => 'Выгрузка', 23 | 'Stop generation' => 'Остановить процесс', 24 | ]; 25 | -------------------------------------------------------------------------------- /src/views/_form.php: -------------------------------------------------------------------------------- 1 | 'queueId']); 6 | 7 | ?> 8 | 9 |
10 |
11 | 0% 12 |
13 |
14 | 15 | 18 |
19 | 20 |
21 | 22 | 50 | --------------------------------------------------------------------------------