Passed
Push — master ( 508a77...6e1f19 )
by Thomas
15:31 queued 01:33
created

ExcelGridFieldExportButton   F

Complexity

Total Complexity 66

Size/Duplication

Total Lines 498
Duplicated Lines 0 %

Importance

Changes 8
Bugs 3 Features 0
Metric Value
eloc 181
c 8
b 3
f 0
dl 0
loc 498
rs 3.12
wmc 66

26 Methods

Rating   Name   Duplication   Size   Complexity  
A setListFilters() 0 4 1
A setHasHeader() 0 4 1
F generateExportFileData() 0 154 35
A getActions() 0 3 1
A getActionName() 0 4 1
A getAfterExportCallback() 0 3 1
A getListFilters() 0 3 1
A setExportType() 0 4 1
A setCheckCanView() 0 4 1
A setButtonTitle() 0 4 1
A setExportName() 0 4 1
A getIsLimited() 0 3 1
A handleExport() 0 32 5
A handleAction() 0 8 2
A getExportColumns() 0 3 1
A getExportType() 0 3 1
A getHasHeader() 0 3 1
A getURLHandlers() 0 3 1
A setAfterExportCallback() 0 4 1
A getCheckCanView() 0 3 1
A getHTMLFragments() 0 21 2
A getExportName() 0 3 1
A setIsLimited() 0 4 1
A setExportColumns() 0 4 1
A getButtonTitle() 0 3 1
A __construct() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like ExcelGridFieldExportButton often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ExcelGridFieldExportButton, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace LeKoala\ExcelImportExport;
4
5
use PhpOffice\PhpSpreadsheet\IOFactory;
6
use SilverStripe\Assets\FileNameFilter;
7
use PhpOffice\PhpSpreadsheet\Spreadsheet;
8
use SilverStripe\Forms\GridField\GridField;
9
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
10
use SilverStripe\Forms\GridField\GridFieldPaginator;
11
use SilverStripe\Forms\GridField\GridField_FormAction;
12
use SilverStripe\Forms\GridField\GridField_URLHandler;
13
use SilverStripe\Forms\GridField\GridFieldFilterHeader;
14
use SilverStripe\Forms\GridField\GridField_HTMLProvider;
15
use SilverStripe\Forms\GridField\GridFieldSortableHeader;
16
use SilverStripe\Forms\GridField\GridField_ActionProvider;
17
use Exception;
18
use SilverStripe\ORM\DataList;
19
20
/**
21
 * Adds an "Export list" button to the bottom of a {@link GridField}.
22
 */
23
class ExcelGridFieldExportButton implements
24
    GridField_HTMLProvider,
25
    GridField_ActionProvider,
26
    GridField_URLHandler
27
{
28
    /**
29
     * @var array Map of a property name on the exported objects, with values being the column title in the file.
30
     * Note that titles are only used when {@link $hasHeader} is set to TRUE.
31
     */
32
    protected $exportColumns;
33
34
    /**
35
     * Fragment to write the button to
36
     */
37
    protected $targetFragment;
38
39
    /**
40
     * @var boolean
41
     */
42
    protected $hasHeader = true;
43
44
    /**
45
     * @var string
46
     */
47
    protected $exportType = 'xlsx';
48
49
    /**
50
     * @var string
51
     */
52
    protected $exportName = null;
53
54
    /**
55
     *
56
     * @var string
57
     */
58
    protected $buttonTitle = null;
59
60
    /**
61
     *
62
     * @var bool
63
     */
64
    protected $checkCanView = true;
65
66
    /**
67
     * @var bool
68
     */
69
    protected $isLimited = true;
70
71
    /**
72
     *
73
     * @var array
74
     */
75
    protected $listFilters = array();
76
77
    /**
78
     *
79
     * @var callable
80
     */
81
    protected $afterExportCallback;
82
83
    /**
84
     * @param string $targetFragment The HTML fragment to write the button into
85
     * @param array $exportColumns The columns to include in the export
86
     */
87
    public function __construct($targetFragment = "after", $exportColumns = null)
88
    {
89
        $this->targetFragment = $targetFragment;
90
        $this->exportColumns = $exportColumns;
91
    }
92
93
    /**
94
     * @param GridField $gridField
95
     * @return string
96
     */
97
    public function getActionName($gridField)
98
    {
99
        $name = strtolower($gridField->getName());
100
        return 'excelexport_' . $name;
101
    }
102
103
    /**
104
     * Place the export button in a <p> tag below the field
105
     */
106
    public function getHTMLFragments($gridField)
107
    {
108
        $title = $this->buttonTitle ? $this->buttonTitle : _t(
109
            'ExcelImportExport.XLSEXPORT',
110
            'Export to Excel'
111
        );
112
113
        $name = $this->getActionName($gridField);
114
115
        $button = new GridField_FormAction(
116
            $gridField,
117
            $name,
118
            $title,
119
            $name,
120
            null
121
        );
122
        $button->addExtraClass('btn btn-secondary no-ajax font-icon-down-circled action_export');
123
        $button->setForm($gridField->getForm());
124
125
        return array(
126
            $this->targetFragment => $button->Field()
127
        );
128
    }
129
130
    /**
131
     * export is an action button
132
     */
133
    public function getActions($gridField)
134
    {
135
        return array($this->getActionName($gridField));
136
    }
137
138
    public function handleAction(
139
        GridField $gridField,
140
        $actionName,
141
        $arguments,
142
        $data
143
    ) {
144
        if (in_array($actionName, $this->getActions($gridField))) {
145
            return $this->handleExport($gridField);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->handleExport($gridField) targeting LeKoala\ExcelImportExpor...tButton::handleExport() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
146
        }
147
    }
148
149
    /**
150
     * it is also a URL
151
     */
152
    public function getURLHandlers($gridField)
153
    {
154
        return array($this->getActionName($gridField) => 'handleExport');
155
    }
156
157
    /**
158
     * Handle the export, for both the action button and the URL
159
     */
160
    public function handleExport($gridField, $request = null)
161
    {
162
        $now = Date("Ymd_Hi");
163
164
        if ($excel = $this->generateExportFileData($gridField)) {
165
            $ext = $this->exportType;
166
            $name = $this->exportName;
167
            $fileName = "$name-$now.$ext";
168
169
            switch ($ext) {
170
                case 'xls':
171
                    $writer = IOFactory::createWriter($excel, 'Xls');
172
                    break;
173
                case 'xlsx':
174
                    $writer = IOFactory::createWriter($excel, 'Xlsx');
175
                    break;
176
                default:
177
                    throw new Exception("$ext is not supported");
178
            }
179
            if ($this->afterExportCallback) {
180
                $func = $this->afterExportCallback;
181
                $func();
182
            }
183
184
            header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
185
            header("Cache-Control: post-check=0, pre-check=0", false);
186
            header("Pragma: no-cache");
187
            header('Content-type: application/vnd.ms-excel');
188
            header('Content-Disposition: attachment; filename="' . $fileName . '"');
189
190
            $writer->save('php://output');
191
            exit();
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
192
        }
193
    }
194
195
    /**
196
     * Generate export fields for Excel.
197
     *
198
     * @param GridField $gridField
199
     * @return Spreadsheet
200
     */
201
    public function generateExportFileData($gridField)
202
    {
203
        $class = $gridField->getModelClass();
204
        $columns = ($this->exportColumns) ? $this->exportColumns : ExcelImportExport::exportFieldsForClass($class);
205
206
        $singl = singleton($class);
207
208
        $plural = $class ? $singl->i18n_plural_name() : '';
209
210
        $filter = new FileNameFilter;
211
        if ($this->exportName) {
212
            $this->exportName = $filter->filter($this->exportName);
213
        } else {
214
            $this->exportName = $filter->filter('export-' . $plural);
215
        }
216
217
        $excel = new Spreadsheet();
218
        $excelProperties = $excel->getProperties();
219
        $excelProperties->setTitle($this->exportName);
220
221
        $sheet = $excel->getActiveSheet();
222
        if ($plural) {
223
            $sheet->setTitle($plural);
224
        }
225
226
        $row = 1;
227
        $col = 1;
228
229
        if ($this->hasHeader) {
230
            $headers = array();
231
232
            // determine the headers. If a field is callable (e.g. anonymous function) then use the
233
            // source name as the header instead
234
            foreach ($columns as $columnSource => $columnHeader) {
235
                $headers[] = (!is_string($columnHeader) && is_callable($columnHeader))
236
                    ? $columnSource : $columnHeader;
237
            }
238
239
            foreach ($headers as $header) {
240
                $sheet->setCellValue([$col, $row], $header);
241
                $col++;
242
            }
243
244
            $endcol = Coordinate::stringFromColumnIndex($col - 1);
245
            $sheet->setAutoFilter("A1:{$endcol}1");
246
            $sheet->getStyle("A1:{$endcol}1")->getFont()->setBold(true);
247
248
            $col = 1;
249
            $row++;
250
        }
251
252
        // Autosize
253
        $cellIterator = $sheet->getRowIterator()->current()->getCellIterator();
254
        try {
255
            $cellIterator->setIterateOnlyExistingCells(true);
256
        } catch (Exception $ex) {
257
            // Ignore exceptions
258
        }
259
        foreach ($cellIterator as $cell) {
260
            $sheet->getColumnDimension($cell->getColumn())->setAutoSize(true);
0 ignored issues
show
Bug introduced by
The method getColumn() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

260
            $sheet->getColumnDimension($cell->/** @scrutinizer ignore-call */ getColumn())->setAutoSize(true);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
261
        }
262
263
        //Remove GridFieldPaginator as we're going to export the entire list.
264
        $gridField->getConfig()->removeComponentsByType(GridFieldPaginator::class);
265
266
        $items = $gridField->getManipulatedList();
267
268
        // @todo should GridFieldComponents change behaviour based on whether others are available in the config?
269
        foreach ($gridField->getConfig()->getComponents() as $component) {
270
            if ($component instanceof GridFieldFilterHeader || $component instanceof GridFieldSortableHeader) {
271
                $items = $component->getManipulatedData($gridField, $items);
272
            }
273
        }
274
275
        $list = $items;
276
        $limit = ExcelImportExport::$limit_exports;
277
        if ($items instanceof DataList && $this->isLimited && $limit > 0) {
278
            $list = $items->limit($limit);
279
            if (!empty($this->listFilters)) {
280
                $list = $list->filter($this->listFilters);
281
            }
282
        }
283
284
        if (!$list) {
285
            return $excel;
286
        }
287
288
        $exportFormat = ExcelImportExport::config()->export_format;
289
290
        foreach ($list as $item) {
291
            if ($this->checkCanView) {
292
                $canView = true;
293
                if ($item->hasMethod('canView') && !$item->canView()) {
294
                    $canView = false;
295
                }
296
                if (!$canView) {
297
                    continue;
298
                }
299
            }
300
            foreach ($columns as $columnSource => $columnHeader) {
301
                if (!is_string($columnHeader) && is_callable($columnHeader)) {
302
                    if ($item->hasMethod($columnSource)) {
303
                        $relObj = $item->{$columnSource}();
304
                    } else {
305
                        $relObj = $item->relObject($columnSource);
306
                    }
307
308
                    $value = $columnHeader($relObj);
309
                } else {
310
                    if (is_string($columnSource)) {
311
                        // It can be a method
312
                        if (strpos($columnSource, '(') !== false) {
313
                            $matches = [];
314
                            preg_match('/([a-zA-Z]*)\((.*)\)/', $columnSource, $matches);
315
                            $func = $matches[1];
316
                            $params = explode(",", $matches[2]);
317
                            // Support only one param for now
318
                            $value = $item->$func($params[0]);
319
                        } else {
320
                            if (array_key_exists($columnSource, $exportFormat)) {
321
                                $format = $exportFormat[$columnSource];
322
                                $value = $item->dbObject($columnSource)->$format();
323
                            } else {
324
                                $value = $gridField->getDataFieldValue($item, $columnSource);
325
                            }
326
                        }
327
                    } else {
328
                        // We can also use a simple dot notation
329
                        $parts = explode(".", $columnHeader);
330
                        if (count($parts) == 1) {
331
                            $value = $item->$columnHeader;
332
                        } else {
333
                            $value = $item->relObject($parts[0]);
334
                            if ($value) {
335
                                $relObjField = $parts[1];
336
                                $value = $value->$relObjField;
337
                            }
338
                        }
339
                    }
340
                }
341
342
                $sheet->setCellValue([$col, $row], $value);
343
                $col++;
344
            }
345
346
            if ($item->hasMethod('destroy')) {
347
                $item->destroy();
348
            }
349
350
            $col = 1;
351
            $row++;
352
        }
353
354
        return $excel;
355
    }
356
357
    /**
358
     * @return array
359
     */
360
    public function getExportColumns()
361
    {
362
        return $this->exportColumns;
363
    }
364
365
    /**
366
     * @param array
367
     */
368
    public function setExportColumns($cols)
369
    {
370
        $this->exportColumns = $cols;
371
        return $this;
372
    }
373
374
    /**
375
     * @return boolean
376
     */
377
    public function getHasHeader()
378
    {
379
        return $this->hasHeader;
380
    }
381
382
    /**
383
     * @param boolean
384
     */
385
    public function setHasHeader($bool)
386
    {
387
        $this->hasHeader = $bool;
388
        return $this;
389
    }
390
391
    /**
392
     * @return string
393
     */
394
    public function getExportType()
395
    {
396
        return $this->exportType;
397
    }
398
399
    /**
400
     * @param string xlsx (default) or xls
0 ignored issues
show
Bug introduced by
The type LeKoala\ExcelImportExport\xlsx was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
401
     */
402
    public function setExportType($exportType)
403
    {
404
        $this->exportType = $exportType;
405
        return $this;
406
    }
407
408
    /**
409
     * @return string
410
     */
411
    public function getExportName()
412
    {
413
        return $this->exportName;
414
    }
415
416
    /**
417
     * @param string $exportName
418
     * @return ExcelGridFieldExportButton
419
     */
420
    public function setExportName($exportName)
421
    {
422
        $this->exportName = $exportName;
423
        return $this;
424
    }
425
426
    /**
427
     * @return string
428
     */
429
    public function getButtonTitle()
430
    {
431
        return $this->buttonTitle;
432
    }
433
434
    /**
435
     * @param string $buttonTitle
436
     * @return ExcelGridFieldExportButton
437
     */
438
    public function setButtonTitle($buttonTitle)
439
    {
440
        $this->buttonTitle = $buttonTitle;
441
        return $this;
442
    }
443
444
    /**
445
     *
446
     * @return bool
447
     */
448
    public function getCheckCanView()
449
    {
450
        return $this->checkCanView;
451
    }
452
453
    /**
454
     *
455
     * @param bool $checkCanView
456
     * @return ExcelGridFieldExportButton
457
     */
458
    public function setCheckCanView($checkCanView)
459
    {
460
        $this->checkCanView = $checkCanView;
461
        return $this;
462
    }
463
464
    /**
465
     *
466
     * @return array
467
     */
468
    public function getListFilters()
469
    {
470
        return $this->listFilters;
471
    }
472
473
    /**
474
     *
475
     * @param array $listFilters
476
     * @return ExcelGridFieldExportButton
477
     */
478
    public function setListFilters($listFilters)
479
    {
480
        $this->listFilters = $listFilters;
481
        return $this;
482
    }
483
484
    /**
485
     *
486
     * @return callable
487
     */
488
    public function getAfterExportCallback()
489
    {
490
        return $this->afterExportCallback;
491
    }
492
493
    /**
494
     *
495
     * @param callable $afterExportCallback
496
     * @return ExcelGridFieldExportButton
497
     */
498
    public function setAfterExportCallback(callable $afterExportCallback)
499
    {
500
        $this->afterExportCallback = $afterExportCallback;
501
        return $this;
502
    }
503
504
    /**
505
     * Get the value of isLimited
506
     */
507
    public function getIsLimited(): bool
508
    {
509
        return $this->isLimited;
510
    }
511
512
    /**
513
     * Set the value of isLimited
514
     *
515
     * @param bool $isLimited
516
     */
517
    public function setIsLimited(bool $isLimited)
518
    {
519
        $this->isLimited = $isLimited;
520
        return $this;
521
    }
522
}
523