Passed
Push — master ( 6f54ed...c693b3 )
by Nicolaas
02:30
created

ExportAllCustomButton::generateExportFileData()   B

Complexity

Conditions 8
Paths 9

Size

Total Lines 52
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 28
c 1
b 0
f 0
nc 9
nop 1
dl 0
loc 52
rs 8.4444

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Sunnysideup\ExportAllFromModelAdmin;
4
5
use League\Csv\Writer;
6
use LogicException;
7
use SilverStripe\Control\HTTPRequest;
8
use SilverStripe\Control\HTTPResponse;
9
use SilverStripe\Core\Config\Config;
10
use SilverStripe\Forms\GridField\AbstractGridFieldComponent;
0 ignored issues
show
Bug introduced by
The type SilverStripe\Forms\GridF...tractGridFieldComponent 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...
11
use SilverStripe\Forms\GridField\GridField;
12
use SilverStripe\Forms\GridField\GridField_ActionProvider;
13
use SilverStripe\Forms\GridField\GridField_FormAction;
14
use SilverStripe\Forms\GridField\GridField_HTMLProvider;
15
use SilverStripe\Forms\GridField\GridField_URLHandler;
16
use SilverStripe\Forms\GridField\GridFieldDataColumns;
17
use SilverStripe\Forms\GridField\GridFieldExportButton;
18
use SilverStripe\Forms\GridField\GridFieldFilterHeader;
19
use SilverStripe\Forms\GridField\GridFieldPaginator;
20
use SilverStripe\Forms\GridField\GridFieldSortableHeader;
21
use SilverStripe\ORM\DB;
22
use SilverStripe\Security\Member;
23
use Sunnysideup\ExportAllFromModelAdmin\Api\AllFields;
24
25
class ExportAllCustomButton extends GridFieldExportButton
26
{
27
28
    /**
29
     * Example:
30
     *
31
     * ```php
32
     * Member::class => [
33
     *     'Name' => [
34
     *         'FirstName',
35
     *         'Surname',
36
     *         'MyHasOneSalutation.Title',
37
     *     ],
38
     *     'Email' => 'Email',
39
     *     'MyHasOneRelation' => 'MyHasOneRelation.Title',
40
     *     'MyManyManyRelation' => 'MyManyManyRelation.Title',
41
     *     'MyManyManyRelation Nice Title' => 'MyManyManyRelation2.Title',
42
     * ],
43
     * MyOtherClass => '*',
44
     * ```
45
     * @var array
46
     */
47
    private static array $custom_exports = [];
48
    private static int $limit_to_lookups = 500;
49
    private static int $limit_to_join_tables = 100000;
50
51
    private static int $max_chars_per_cell = 200;
52
    private static $db_defaults = [
53
        'ID' => 'Int',
54
        'Created' => 'DBDatetime',
55
        'LastEdited' => 'DBDatetime',
56
    ];
57
58
    protected bool $hasCustomExport = false;
59
    protected array $dbCache = [];
60
    protected array $relCache = [];
61
62
    protected array $lookupTableCache = [];
63
64
    protected array $joinTableCache = [];
65
    protected string $exportSeparator = ' ||| ';
66
67
68
    /**
69
     * Generate export fields for CSV.
70
     *
71
     * @param GridField $gridField
72
     *
73
     * @return string
74
     */
75
    public function generateExportFileData($gridField): string
76
    {
77
        $modelClass = $gridField->getModelClass();
78
        $custom = Config::inst()->get(static::class, 'custom_exports');
79
        if (empty($custom[$modelClass]) || ! is_array($custom[$modelClass])) {
80
            return parent::generateExportFileData($gridField);
81
        }
82
83
        // set basic variables
84
        $this->hasCustomExport = true;
85
        $this->exportColumns = $custom[$modelClass];
86
        $this->exportSeparator = ' ' . Config::inst()->get(ExportAllFromModelAdminTraitSettings::class, 'export_separator') . ' ';
87
        $this->buildRelCache();
88
89
        // basics -- see parent::generateExportFileData
90
        $csvWriter = Writer::createFromFileObject(new \SplTempFileObject());
91
        $csvWriter->setDelimiter($this->getCsvSeparator());
92
        $csvWriter->setEnclosure($this->getCsvEnclosure());
93
        $csvWriter->setOutputBOM(Writer::BOM_UTF8);
94
95
        if (!Config::inst()->get(static::class, 'xls_export_disabled')) {
96
            $csvWriter->addFormatter(function (array $row) {
97
                foreach ($row as &$item) {
98
                    // [SS-2017-007] Sanitise XLS executable column values with a leading tab
99
                    if (preg_match('/^[-@=+].*/', $item ?? '')) {
100
                        $item = "\t" . $item;
101
                    }
102
                }
103
                return $row;
104
            });
105
        }
106
107
108
        //Remove GridFieldPaginator as we're going to export the entire list.
109
        $gridField->getConfig()->removeComponentsByType(GridFieldPaginator::class);
110
        $items = $gridField->getManipulatedList()->limit(null);
0 ignored issues
show
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

110
        $items = $gridField->getManipulatedList()->/** @scrutinizer ignore-call */ limit(null);
Loading history...
111
112
        // set header
113
        $columnData = array_keys($this->exportColumns);
114
        $csvWriter->insertOne($columnData);
115
116
        // add items
117
        foreach ($items as $item) {
118
            $columnData = $this->getDataRowForExport($item);
119
            $csvWriter->insertOne($columnData);
120
        }
121
122
        if (method_exists($csvWriter, 'toString')) {
123
            return $csvWriter->toString();
124
        }
125
126
        return (string)$csvWriter;
127
    }
128
129
130
    protected function getDataRowForExport($item)
131
    {
132
        $array = [];
133
        $maxCharsPerCell = Config::inst()->get(static::class, 'max_chars_per_cell');
134
        foreach ($this->exportColumns as $fieldOrFieldArray) {
135
            $v = $this->getDataRowForExportInner($item, $fieldOrFieldArray);
136
            $v = substr($v, 0, $maxCharsPerCell);
137
            $array[] = $v;
138
        }
139
        return $array;
140
    }
141
142
    protected function getDataRowForExportInner($item, $fieldOrFieldArray): string
143
    {
144
        if (!$fieldOrFieldArray) {
145
            return '';
146
        }
147
        if (is_array($fieldOrFieldArray)) {
148
            $array = [];
149
            foreach ($fieldOrFieldArray as $key => $field) {
150
                $v = '';
151
                if ($key !== intval($key)) {
152
                    $v .= $key . ': ';
153
                }
154
                $v .= $this->getDataRowForExportInner($item, $field);
155
                $array[] = $v;
156
            }
157
            return (string) implode($this->exportSeparator, array_filter($array));
158
        } elseif (strpos($fieldOrFieldArray, '.') !== false) {
159
            return (string) $this->fetchRelData($item, $fieldOrFieldArray);
160
        } else {
161
            $type = $this->fieldTypes($fieldOrFieldArray);
162
            if (strpos($type, 'Boolean') !== false) {
163
                return (string) ($item->$fieldOrFieldArray ? 'Yes' : 'No');
164
            }
165
            return (string) $item->$fieldOrFieldArray;
166
        }
167
    }
168
169
170
    protected function fetchRelData($item, string $hasOneString): string
171
    {
172
        $hasOneArray = explode('.', $hasOneString);
173
        $methodName = array_shift($hasOneArray);
174
        $foreignField = $hasOneArray[0];
175
        $relType = $this->getRelationshipType($methodName);
176
        $className = $this->getRelClassName($methodName);
177
        if (!isset($this->lookupTableCache[$className])) {
178
            $limit = Config::inst()->get(static::class, 'limit_to_lookups');
179
            $this->lookupTableCache[$className] = $className::get()->limit($limit)->map('ID', $foreignField)->toArray();
180
        }
181
        if ($relType === 'has_one') {
182
            // Check if data is already cached
183
            $fieldName = $methodName . 'ID';
184
            $id = (int) $item->$fieldName;
185
            if ($id === 0) {
186
                return '';
187
            }
188
            return (string) ($this->lookupTableCache[$className][$id] ?? 'error' . $className::get()->byID($id)?->$foreignField);
189
        } else {
190
            $result = [];
191
            // slow....
192
            if ($relType === 'has_many') {
193
                foreach ($item->$methodName()->column($foreignField) as $val) {
194
                    $result[] = $val;
195
                }
196
            } elseif ($relType === 'many_many') {
197
                if (!isset($this->joinTableCache[$className])) {
198
                    $rel = $item->$methodName();
199
                    $this->joinTableCache[$className] = [
200
                        'table' => $rel->getJoinTable(),
201
                        'local' => $rel->getLocalKey(),
202
                        'foreign' => $rel->getForeignKey(),
203
                    ];
204
                    $joinTable = $this->joinTableCache[$className]['table'];
205
                    $localIDField = $this->joinTableCache[$className]['local'];
206
                    $foreignIDField = $this->joinTableCache[$className]['foreign'];
207
                    $limit = Config::inst()->get(static::class, 'limit_to_join_tables');
208
                    $list = DB::query('SELECT "' . $localIDField . '", "' . $foreignIDField . '" FROM "' . $joinTable . '" LIMIT ' . $limit);
209
                    foreach ($list as $row) {
210
                        if (! isset($this->lookupTableCache[$joinTable][$row[$localIDField]])) {
211
                            $this->lookupTableCache[$joinTable][$row[$localIDField]] = [];
212
                        }
213
                        $this->lookupTableCache[$joinTable][$row[$localIDField]][] = $row[$foreignIDField];
214
                    }
215
                } else {
216
                    $joinTable = $this->joinTableCache[$className]['table'];
217
                    $foreignIDField = $this->joinTableCache[$className]['foreign'];
0 ignored issues
show
Unused Code introduced by
The assignment to $foreignIDField is dead and can be removed.
Loading history...
218
                }
219
                if (! empty($this->lookupTableCache[$joinTable][$item->ID])) {
220
                    foreach ($this->lookupTableCache[$joinTable][$item->ID] as $foreignID) {
221
                        $result[] = $this->lookupTableCache[$className][$foreignID] ?? '';
222
                    }
223
                }
224
            }
225
            return implode($this->exportSeparator, $result);
226
        }
227
    }
228
229
    protected function fieldTypes($fieldName)
230
    {
231
232
        if (count($this->dbCache) === 0) {
233
            $this->dbCache =
234
                Config::inst()->get(static::class, 'db_defaults') +
235
                Config::inst()->get(Member::class, 'db');
236
        }
237
        return $this->dbCache[$fieldName];
238
    }
239
240
    protected function getRelationshipType($methodName)
241
    {
242
        return $this->relCache[$methodName]['type'];
243
    }
244
245
    protected function getRelClassName($methodName)
246
    {
247
        return $this->relCache[$methodName]['class'];
248
    }
249
250
    protected function buildRelCache()
251
    {
252
        if (count($this->relCache) === 0) {
253
254
            foreach (['has_one', 'has_many', 'many_many'] as $relType) {
255
                foreach (Config::inst()->get(Member::class, $relType) as $methodName => $className) {
256
                    $this->relCache[$methodName] = [
257
                        'type' => $relType,
258
                        'class' => $className
259
                    ];
260
                }
261
            }
262
        }
263
    }
264
265
    /**
266
     * Return the columns to export
267
     *
268
     * @param GridField $gridField
269
     *
270
     * @return array
271
     */
272
    protected function getExportColumnsForGridField(GridField $gridField)
273
    {
274
        $modelClass = $gridField->getModelClass();
275
        $custom = Config::inst()->get(static::class, 'custom_exports');
276
        if (isset($custom[$modelClass]) && $custom[$modelClass] === '*') {
277
            $this->exportColumns = AllFields::create($modelClass)->getExportFields();
278
        }
279
        return parent::getExportColumnsForGridField($gridField);
280
    }
281
}
282