FastExportButton::setExportColumns()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 4
rs 10
1
<?php
2
3
namespace LeKoala\DevToolkit\Buttons;
4
5
use SilverStripe\ORM\DB;
6
use SilverStripe\ORM\ArrayLib;
7
use SilverStripe\Core\ClassInfo;
8
use SilverStripe\ORM\DataObject;
9
use SilverStripe\Core\Config\Config;
10
use SilverStripe\Control\HTTPRequest;
11
use SilverStripe\Assets\FileNameFilter;
12
use SilverStripe\Subsites\Model\Subsite;
0 ignored issues
show
Bug introduced by
The type SilverStripe\Subsites\Model\Subsite 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...
13
use SilverStripe\Forms\GridField\GridField;
14
use SilverStripe\Forms\GridField\GridField_FormAction;
15
use SilverStripe\Forms\GridField\GridField_URLHandler;
16
use SilverStripe\Forms\GridField\GridField_HTMLProvider;
17
use SilverStripe\Forms\GridField\GridField_ActionProvider;
18
19
/**
20
 * Adds an "Fast Export" button to the bottom of a {@link GridField}.
21
 *
22
 * It performs a raw query on the table instead of trying to iterate over a list of objects
23
 */
24
class FastExportButton implements
25
    GridField_HTMLProvider,
26
    GridField_ActionProvider,
27
    GridField_URLHandler
28
{
29
30
    /**
31
     * @var string
32
     */
33
    protected $csvSeparator = ",";
34
35
    /**
36
     * @var array Map of a property name on the exported objects, with values being the column title in the file.
37
     * Note that titles are only used when {@link $hasHeader} is set to TRUE.
38
     */
39
    protected $exportColumns;
40
41
    /**
42
     * Fragment to write the button to
43
     */
44
    protected $targetFragment;
45
46
    /**
47
     * @var boolean
48
     */
49
    protected $hasHeader = true;
50
51
    /**
52
     * @var string
53
     */
54
    protected $exportName = null;
55
56
    /**
57
     *
58
     * @var string
59
     */
60
    protected $buttonTitle = null;
61
62
    /**
63
     *
64
     * @var array
65
     */
66
    protected $listFilters = array();
67
68
    /**
69
     * Static instance counter to allow multiple instances to work together
70
     * @var int
71
     */
72
    protected static $instances = 0;
73
74
    /**
75
     * Current instance count
76
     * @var int
77
     */
78
    protected $instance;
79
80
    /**
81
     * @param string $targetFragment The HTML fragment to write the button into
82
     * @param array $exportColumns The columns to include in the export
83
     */
84
    public function __construct($targetFragment = "after", $exportColumns = null)
85
    {
86
        $this->targetFragment = $targetFragment;
87
        $this->exportColumns = $exportColumns;
88
        self::$instances++;
89
        $this->instance = self::$instances;
90
    }
91
92
    public function getActionName()
93
    {
94
        return 'fastexport_' . $this->instance;
95
    }
96
97
    /**
98
     * Place the export button in a <p> tag below the field
99
     */
100
    public function getHTMLFragments($gridField)
101
    {
102
        $title = $this->buttonTitle ? $this->buttonTitle : _t(
103
            'TableListField.FASTEXPORT',
104
            'Fast Export'
105
        );
106
107
        $name = $this->getActionName();
108
109
        $button = new GridField_FormAction(
110
            $gridField,
111
            $name,
112
            $title,
113
            $name,
114
            null
115
        );
116
        $button->addExtraClass('no-ajax action_export');
117
        $button->setForm($gridField->getForm());
118
119
        return array(
120
            $this->targetFragment => '<p class="grid-fastexport-button">' . $button->Field() . '</p>',
121
        );
122
    }
123
124
    /**
125
     * export is an action button
126
     */
127
    public function getActions($gridField)
128
    {
129
        return array($this->getActionName());
130
    }
131
132
    public function handleAction(
133
        GridField $gridField,
134
        $actionName,
135
        $arguments,
136
        $data
137
    ) {
138
        if (in_array($actionName, $this->getActions($gridField))) {
139
            return $this->handleExport($gridField);
140
        }
141
    }
142
143
    /**
144
     * it is also a URL
145
     */
146
    public function getURLHandlers($gridField)
147
    {
148
        return array($this->getActionName() => 'handleExport');
149
    }
150
151
    /**
152
     * Handle the export, for both the action button and the URL
153
     */
154
    public function handleExport($gridField, $request = null)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

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

154
    public function handleExport($gridField, /** @scrutinizer ignore-unused */ $request = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
155
    {
156
        $now = Date("Ymd_Hi");
157
158
        if ($fileData = $this->generateExportFileData($gridField)) {
159
            $name = $this->exportName;
160
            $ext = 'csv';
161
            $fileName = "$name-$now.$ext";
162
163
            return HTTPRequest::send_file($fileData, $fileName, 'text/csv');
164
        }
165
    }
166
167
    public static function allFieldsForClass($class)
168
    {
169
        $dataClasses = ClassInfo::dataClassesFor($class);
170
        $fields = array();
171
        foreach ($dataClasses as $dataClass) {
172
            $databaseFields = DataObject::getSchema()->databaseFields($dataClass);
173
174
            $dataFields = [];
175
            foreach ($databaseFields as $name => $type) {
176
                if ($type == 'Text' || $type == 'HTMLText') {
177
                    continue;
178
                }
179
                $dataFields[] = $name;
180
            }
181
            $fields = array_merge(
182
                $fields,
183
                $dataFields
184
            );
185
        }
186
        return array_combine($fields, $fields);
187
    }
188
189
    public static function exportFieldsForClass($class)
190
    {
191
        $singl = singleton($class);
192
        if ($singl->hasMethod('exportedFields')) {
193
            return $singl->exportedFields();
194
        }
195
        $exportedFields = Config::inst()->get($class, 'exported_fields');
196
        if (!$exportedFields) {
197
            $exportedFields = array_keys(self::allFieldsForClass($class));
198
        }
199
        $unexportedFields = Config::inst()->get($class, 'unexported_fields');
200
        if ($unexportedFields) {
201
            $exportedFields = array_diff($exportedFields, $unexportedFields);
202
        }
203
        return array_combine($exportedFields, $exportedFields);
204
    }
205
206
    /**
207
     * Generate export fields
208
     *
209
     * @param GridField $gridField
210
     * @return string
211
     */
212
    public function generateExportFileData($gridField)
213
    {
214
        $class = $gridField->getModelClass();
215
        $columns = ($this->exportColumns) ? $this->exportColumns : self::exportFieldsForClass($class);
216
        $fileData = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $fileData is dead and can be removed.
Loading history...
217
218
        // If we don't have an associative array
219
        if (!ArrayLib::is_associative($columns)) {
220
            array_combine($columns, $columns);
221
        }
222
223
        $singl = singleton($class);
224
225
        $singular = $class ? $singl->i18n_singular_name() : '';
0 ignored issues
show
Unused Code introduced by
The assignment to $singular is dead and can be removed.
Loading history...
226
        $plural = $class ? $singl->i18n_plural_name() : '';
227
228
        $filter = new FileNameFilter;
229
        if ($this->exportName) {
230
            $this->exportName = $filter->filter($this->exportName);
231
        } else {
232
            $this->exportName = $filter->filter('fastexport-' . $plural);
233
        }
234
235
        $fileData = '';
236
        $separator = $this->csvSeparator;
0 ignored issues
show
Unused Code introduced by
The assignment to $separator is dead and can be removed.
Loading history...
237
238
        $class = $gridField->getModelClass();
239
        $singl = singleton($class);
240
        $baseTable = $singl->baseTable();
241
242
        $stream = fopen('data://text/plain,' . "", 'w+');
243
244
        // Filter columns
245
        $sqlFields = [];
246
        $baseFields = ['ID', 'Created', 'LastEdited'];
247
248
        $joins = [];
249
        $isSubsite = false;
250
        $map = [];
251
        if ($singl->hasMethod('fastExportMap')) {
252
            $map = $singl->fastExportMap();
253
        }
254
        foreach ($columns as $columnSource => $columnHeader) {
255
            // Allow mapping methods to plain fields
256
            if ($map && isset($map[$columnSource])) {
257
                $columnSource = $map[$columnSource];
258
            }
259
            if ($columnSource == 'SubsiteID') {
260
                $isSubsite = true;
261
            }
262
            if (in_array($columnSource, $baseFields)) {
263
                $sqlFields[] = $baseTable . '.' . $columnSource;
264
                continue;
265
            }
266
            // Naive join support
267
            if (strpos($columnSource, '.') !== false) {
268
                $parts = explode('.', $columnSource);
269
270
                $joinSingl = singleton($parts[0]);
271
                $joinBaseTable = $joinSingl->baseTable();
272
273
                if (!isset($joins[$joinBaseTable])) {
274
                    $joins[$joinBaseTable] = [];
275
                }
276
                $joins[$joinBaseTable][] = $parts[1];
277
278
                $sqlFields[] = $joinBaseTable . '.' . $parts[1];
279
                continue;
280
            }
281
            $fieldTable = ClassInfo::table_for_object_field($class, $columnSource);
0 ignored issues
show
Bug introduced by
The method table_for_object_field() does not exist on SilverStripe\Core\ClassInfo. ( Ignorable by Annotation )

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

281
            /** @scrutinizer ignore-call */ 
282
            $fieldTable = ClassInfo::table_for_object_field($class, $columnSource);

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...
282
            if ($fieldTable != $baseTable || !$fieldTable) {
283
                unset($columns[$columnSource]);
284
            } else {
285
                $sqlFields[] = $fieldTable . '.' . $columnSource;
286
            }
287
        }
288
289
        if ($this->hasHeader) {
290
            $headers = array();
291
292
            // determine the headers. If a field is callable (e.g. anonymous function) then use the
293
            // source name as the header instead
294
            foreach ($columns as $columnSource => $columnHeader) {
295
                $headers[] = (!is_string($columnHeader) && is_callable($columnHeader))
296
                    ? $columnSource : $columnHeader;
297
            }
298
299
            $row = array_values($headers);
300
            // fputcsv($stream, $row, $separator);
301
302
            // force quotes
303
            fputs($stream, implode(",", array_map("self::encodeFunc", $row)) . "\n");
304
        }
305
306
        if (empty($sqlFields)) {
307
            $sqlFields = ['ID', 'Created', 'LastEdited'];
308
        }
309
310
        $where = [];
311
        $sql = 'SELECT ' . implode(',', $sqlFields) . ' FROM ' . $baseTable;
312
        foreach ($joins as $joinTable => $joinFields) {
313
            $foreignKey = $joinTable . 'ID';
314
            $sql .= ' LEFT JOIN ' . $joinTable . ' ON ' . $joinTable . '.ID = ' . $baseTable . '.' . $foreignKey;
315
        }
316
        // Basic subsite support
317
        if ($isSubsite && class_exists('Subsite') && Subsite::currentSubsiteID()) {
318
            $where[] = $baseTable . '.SubsiteID = ' . Subsite::currentSubsiteID();
319
        }
320
321
        $singl->extend('updateFastExport', $sql, $where);
322
323
        // Basic where clause
324
        if (!empty($where)) {
325
            $sql .= ' WHERE ' . implode(' AND ', $where);
326
        }
327
328
        $query = DB::query($sql);
329
330
        foreach ($query as $row) {
331
            // fputcsv($stream, $row, $separator);
332
333
            // force quotes
334
            fputs($stream, implode(",", array_map("self::encodeFunc", $row)) . "\n");
335
        }
336
337
        rewind($stream);
338
        $fileData = stream_get_contents($stream);
339
        fclose($stream);
340
341
        return $fileData;
342
    }
343
344
    public static function encodeFunc($value)
345
    {
346
        ///remove any ESCAPED double quotes within string.
347
        $value = str_replace('\\"', '"', $value);
348
        //then force escape these same double quotes And Any UNESCAPED Ones.
349
        $value = str_replace('"', '\"', $value);
350
        //force wrap value in quotes and return
351
        return '"' . $value . '"';
352
    }
353
354
    /**
355
     * @return array
356
     */
357
    public function getExportColumns()
358
    {
359
        return $this->exportColumns;
360
    }
361
362
    /**
363
     * @param array
364
     */
365
    public function setExportColumns($cols)
366
    {
367
        $this->exportColumns = $cols;
368
        return $this;
369
    }
370
371
    /**
372
     * @return boolean
373
     */
374
    public function getHasHeader()
375
    {
376
        return $this->hasHeader;
377
    }
378
379
    /**
380
     * @param boolean
381
     */
382
    public function setHasHeader($bool)
383
    {
384
        $this->hasHeader = $bool;
385
        return $this;
386
    }
387
388
    /**
389
     * @return string
390
     */
391
    public function getExportName()
392
    {
393
        return $this->exportName;
394
    }
395
396
    /**
397
     * @param string $exportName
398
     * @return $this
399
     */
400
    public function setExportName($exportName)
401
    {
402
        $this->exportName = $exportName;
403
        return $this;
404
    }
405
406
    /**
407
     * @return string
408
     */
409
    public function getButtonTitle()
410
    {
411
        return $this->buttonTitle;
412
    }
413
414
    /**
415
     * @param string $buttonTitle
416
     * @return $this
417
     */
418
    public function setButtonTitle($buttonTitle)
419
    {
420
        $this->buttonTitle = $buttonTitle;
421
        return $this;
422
    }
423
424
    /**
425
     * @return string
426
     */
427
    public function getCsvSeparator()
428
    {
429
        return $this->csvSeparator;
430
    }
431
432
    /**
433
     * @param string
434
     */
435
    public function setCsvSeparator($separator)
436
    {
437
        $this->csvSeparator = $separator;
438
        return $this;
439
    }
440
}
441