├── .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 | [](https://packagist.org/packages/custom-it/yii2-excel-report)
24 | [](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 |
14 |
15 |
18 |
21 |
22 |
50 |
--------------------------------------------------------------------------------