Passed
Push — 4 ( dace2f...ced2ba )
by Damian
09:35
created

GridFieldExportButton   B

Complexity

Total Complexity 41

Size/Duplication

Total Lines 313
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 313
rs 8.2769
c 0
b 0
f 0
wmc 41

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A getActions() 0 3 1
A getExportColumns() 0 3 1
A setExportColumns() 0 4 1
A getCsvSeparator() 0 3 1
A getURLHandlers() 0 4 1
D generateExportFileData() 0 85 22
A setCsvSeparator() 0 4 1
A getExportColumnsForGridField() 0 13 3
A getHTMLFragments() 0 13 1
A handleAction() 0 6 2
A getCsvEnclosure() 0 3 1
A setCsvEnclosure() 0 4 1
A getCsvHasHeader() 0 3 1
A setCsvHasHeader() 0 4 1
A handleExport() 0 9 2

How to fix   Complexity   

Complex Class

Complex classes like GridFieldExportButton 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 GridFieldExportButton, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\Forms\GridField;
4
5
use League\Csv\Writer;
6
use SilverStripe\Control\HTTPRequest;
7
use SilverStripe\Control\HTTPResponse;
8
use SilverStripe\Core\Config\Config;
9
use SilverStripe\ORM\DataObject;
10
11
/**
12
 * Adds an "Export list" button to the bottom of a {@link GridField}.
13
 */
14
class GridFieldExportButton implements GridField_HTMLProvider, GridField_ActionProvider, GridField_URLHandler
15
{
16
    /**
17
     * @var array Map of a property name on the exported objects, with values being the column title in the CSV file.
18
     * Note that titles are only used when {@link $csvHasHeader} is set to TRUE.
19
     */
20
    protected $exportColumns;
21
22
    /**
23
     * @var string
24
     */
25
    protected $csvSeparator = ",";
26
27
    /**
28
     * @var string
29
     */
30
    protected $csvEnclosure = '"';
31
32
    /**
33
     * @var boolean
34
     */
35
    protected $csvHasHeader = true;
36
37
    /**
38
     * Fragment to write the button to
39
     */
40
    protected $targetFragment;
41
42
    /**
43
     * Set to true to disable XLS sanitisation
44
     * [SS-2017-007] Ensure all cells with leading [@=+] have a leading tab
45
     *
46
     * @config
47
     * @var bool
48
     */
49
    private static $xls_export_disabled = false;
0 ignored issues
show
introduced by
The private property $xls_export_disabled is not used, and could be removed.
Loading history...
50
51
    /**
52
     * @param string $targetFragment The HTML fragment to write the button into
53
     * @param array $exportColumns The columns to include in the export
54
     */
55
    public function __construct($targetFragment = "after", $exportColumns = null)
56
    {
57
        $this->targetFragment = $targetFragment;
58
        $this->exportColumns = $exportColumns;
59
    }
60
61
    /**
62
     * Place the export button in a <p> tag below the field
63
     *
64
     * @param GridField $gridField
65
     *
66
     * @return array
67
     */
68
    public function getHTMLFragments($gridField)
69
    {
70
        $button = new GridField_FormAction(
71
            $gridField,
72
            'export',
73
            _t('SilverStripe\\Forms\\GridField\\GridField.CSVEXPORT', 'Export to CSV'),
74
            'export',
75
            null
76
        );
77
        $button->addExtraClass('btn btn-secondary no-ajax font-icon-down-circled action_export');
78
        $button->setForm($gridField->getForm());
79
        return [
80
            $this->targetFragment => $button->Field(),
81
        ];
82
    }
83
84
    /**
85
     * export is an action button
86
     *
87
     * @param GridField $gridField
88
     *
89
     * @return array
90
     */
91
    public function getActions($gridField)
92
    {
93
        return ['export'];
94
    }
95
96
    public function handleAction(GridField $gridField, $actionName, $arguments, $data)
97
    {
98
        if ($actionName == 'export') {
99
            return $this->handleExport($gridField);
100
        }
101
        return null;
102
    }
103
104
    /**
105
     * it is also a URL
106
     *
107
     * @param GridField $gridField
108
     *
109
     * @return array
110
     */
111
    public function getURLHandlers($gridField)
112
    {
113
        return [
114
            'export' => 'handleExport',
115
        ];
116
    }
117
118
    /**
119
     * Handle the export, for both the action button and the URL
120
     *
121
     * @param GridField $gridField
122
     * @param HTTPRequest $request
123
     *
124
     * @return HTTPResponse
125
     */
126
    public function handleExport($gridField, $request = null)
127
    {
128
        $now = date("d-m-Y-H-i");
129
        $fileName = "export-$now.csv";
130
131
        if ($fileData = $this->generateExportFileData($gridField)) {
132
            return HTTPRequest::send_file($fileData, $fileName, 'text/csv');
133
        }
134
        return null;
135
    }
136
137
    /**
138
     * Return the columns to export
139
     *
140
     * @param GridField $gridField
141
     *
142
     * @return array
143
     */
144
    protected function getExportColumnsForGridField(GridField $gridField)
145
    {
146
        if ($this->exportColumns) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->exportColumns of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
147
            return $this->exportColumns;
148
        }
149
150
        /** @var GridFieldDataColumns $dataCols */
151
        $dataCols = $gridField->getConfig()->getComponentByType(GridFieldDataColumns::class);
152
        if ($dataCols) {
0 ignored issues
show
introduced by
The condition $dataCols can never be true.
Loading history...
153
            return $dataCols->getDisplayFields($gridField);
154
        }
155
156
        return DataObject::singleton($gridField->getModelClass())->summaryFields();
157
    }
158
159
    /**
160
     * Generate export fields for CSV.
161
     *
162
     * @param GridField $gridField
163
     *
164
     * @return string
165
     */
166
    public function generateExportFileData($gridField)
167
    {
168
        $csvColumns = $this->getExportColumnsForGridField($gridField);
169
170
        $csvWriter = Writer::createFromFileObject(new \SplTempFileObject());
171
        $csvWriter->setDelimiter($this->getCsvSeparator());
172
        $csvWriter->setEnclosure($this->getCsvEnclosure());
173
        $csvWriter->setNewline("\r\n"); //use windows line endings for compatibility with some csv libraries
174
        $csvWriter->setOutputBOM(Writer::BOM_UTF8);
175
176
        if (!Config::inst()->get(get_class($this), 'xls_export_disabled')) {
177
            $csvWriter->addFormatter(function (array $row) {
178
                foreach ($row as &$item) {
179
                    // [SS-2017-007] Sanitise XLS executable column values with a leading tab
180
                    if (preg_match('/^[-@=+].*/', $item)) {
181
                        $item = "\t" . $item;
182
                    }
183
                }
184
                return $row;
185
            });
186
        }
187
188
        if ($this->csvHasHeader) {
189
            $headers = [];
190
191
            // determine the CSV headers. If a field is callable (e.g. anonymous function) then use the
192
            // source name as the header instead
193
            foreach ($csvColumns as $columnSource => $columnHeader) {
194
                if (is_array($columnHeader) && array_key_exists('title', $columnHeader)) {
195
                    $headers[] = $columnHeader['title'];
196
                } else {
197
                    $headers[] = (!is_string($columnHeader) && is_callable($columnHeader)) ? $columnSource : $columnHeader;
198
                }
199
            }
200
201
            $csvWriter->insertOne($headers);
202
            unset($headers);
203
        }
204
205
        //Remove GridFieldPaginator as we're going to export the entire list.
206
        $gridField->getConfig()->removeComponentsByType(GridFieldPaginator::class);
207
208
        $items = $gridField->getManipulatedList();
209
210
        // @todo should GridFieldComponents change behaviour based on whether others are available in the config?
211
        foreach ($gridField->getConfig()->getComponents() as $component) {
212
            if ($component instanceof GridFieldFilterHeader || $component instanceof GridFieldSortableHeader) {
213
                $items = $component->getManipulatedData($gridField, $items);
214
            }
215
        }
216
217
        /** @var DataObject $item */
218
        foreach ($items->limit(null) as $item) {
0 ignored issues
show
Bug introduced by
The method limit() does not exist on SilverStripe\ORM\Sortable. Since it exists in all sub-types, consider adding an abstract or default implementation to SilverStripe\ORM\Sortable. ( Ignorable by Annotation )

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

218
        foreach ($items->/** @scrutinizer ignore-call */ limit(null) as $item) {
Loading history...
Bug introduced by
The method limit() does not exist on SilverStripe\ORM\SS_List. It seems like you code against a sub-type of said class. However, the method does not exist in SilverStripe\ORM\Sortable or SilverStripe\ORM\Filterable. Are you sure you never get one of those? ( Ignorable by Annotation )

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

218
        foreach ($items->/** @scrutinizer ignore-call */ limit(null) as $item) {
Loading history...
219
            if (!$item->hasMethod('canView') || $item->canView()) {
220
                $columnData = [];
221
222
                foreach ($csvColumns as $columnSource => $columnHeader) {
223
                    if (!is_string($columnHeader) && is_callable($columnHeader)) {
224
                        if ($item->hasMethod($columnSource)) {
225
                            $relObj = $item->{$columnSource}();
226
                        } else {
227
                            $relObj = $item->relObject($columnSource);
228
                        }
229
230
                        $value = $columnHeader($relObj);
231
                    } else {
232
                        $value = $gridField->getDataFieldValue($item, $columnSource);
233
234
                        if ($value === null) {
235
                            $value = $gridField->getDataFieldValue($item, $columnHeader);
236
                        }
237
                    }
238
239
                    $columnData[] = $value;
240
                }
241
242
                $csvWriter->insertOne($columnData);
243
            }
244
245
            if ($item->hasMethod('destroy')) {
246
                $item->destroy();
247
            }
248
        }
249
250
        return (string)$csvWriter;
251
    }
252
253
    /**
254
     * @return array
255
     */
256
    public function getExportColumns()
257
    {
258
        return $this->exportColumns;
259
    }
260
261
    /**
262
     * @param array $cols
263
     *
264
     * @return $this
265
     */
266
    public function setExportColumns($cols)
267
    {
268
        $this->exportColumns = $cols;
269
        return $this;
270
    }
271
272
    /**
273
     * @return string
274
     */
275
    public function getCsvSeparator()
276
    {
277
        return $this->csvSeparator;
278
    }
279
280
    /**
281
     * @param string $separator
282
     *
283
     * @return $this
284
     */
285
    public function setCsvSeparator($separator)
286
    {
287
        $this->csvSeparator = $separator;
288
        return $this;
289
    }
290
291
    /**
292
     * @return string
293
     */
294
    public function getCsvEnclosure()
295
    {
296
        return $this->csvEnclosure;
297
    }
298
299
    /**
300
     * @param string $enclosure
301
     *
302
     * @return $this
303
     */
304
    public function setCsvEnclosure($enclosure)
305
    {
306
        $this->csvEnclosure = $enclosure;
307
        return $this;
308
    }
309
310
    /**
311
     * @return boolean
312
     */
313
    public function getCsvHasHeader()
314
    {
315
        return $this->csvHasHeader;
316
    }
317
318
    /**
319
     * @param boolean $bool
320
     *
321
     * @return $this
322
     */
323
    public function setCsvHasHeader($bool)
324
    {
325
        $this->csvHasHeader = $bool;
326
        return $this;
327
    }
328
}
329