Passed
Push — master ( 2e22bb...76b84c )
by Nicolaas
02:24
created

ExportAllCustomButton   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 268
Duplicated Lines 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 130
c 2
b 1
f 0
dl 0
loc 268
rs 9.0399
wmc 42

10 Methods

Rating   Name   Duplication   Size   Complexity  
B generateExportFileData() 0 52 8
B getDataRowForExportInner() 0 24 8
A getDataRowForExport() 0 10 2
C fetchRelData() 0 64 12
A classToSafeClass() 0 3 1
A fieldTypes() 0 9 2
A buildRelCache() 0 9 4
A getRelClassName() 0 3 1
A getRelationshipType() 0 3 1
A getExportColumnsForGridField() 0 8 3

How to fix   Complexity   

Complex Class

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

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(100);
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(100);
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 $fieldName): string
171
    {
172
        $fieldNameArray = explode('.', $fieldName);
173
        $methodName = array_shift($fieldNameArray);
174
        $foreignField = $fieldNameArray[0];
175
        $relType = $this->getRelationshipType($methodName);
176
        $className = $this->getRelClassName($methodName);
177
        $classNameForArray = $this->classToSafeClass($className);
178
        // die($methodName . '.' . $foreignField . '.' . $relType . '.' . $className);
179
        if (!isset($this->lookupTableCache[$classNameForArray])) {
180
            $limit = Config::inst()->get(static::class, 'limit_to_lookups');
181
            $this->lookupTableCache[$classNameForArray] = $className::get()->limit($limit)->map('ID', $foreignField)->toArray();
182
        }
183
        if ($relType === 'has_one') {
184
            // Check if data is already cached
185
            $fieldName = $methodName . 'ID';
186
            $id = (int) $item->$fieldName;
187
            if ($id === 0) {
188
                return '';
189
            }
190
            return (string) ($this->lookupTableCache[$classNameForArray][$id] ?? 'error' . $className::get()->byID($id)?->$foreignField);
191
        } else {
192
            $result = [];
193
            // slow....
194
            if ($relType === 'has_many') {
195
                foreach ($item->$methodName()->column($foreignField) as $val) {
196
                    $result[] = $val;
197
                }
198
            } elseif ($relType === 'many_many') {
199
                if (!isset($this->joinTableCache[$classNameForArray])) {
200
                    // relation object details
201
                    $rel = $item->$methodName();
202
                    $this->joinTableCache[$classNameForArray] = [
203
                        'table' => $rel->getJoinTable(),
204
                        'local' => $rel->getLocalKey(),
205
                        'foreign' => $rel->getForeignKey(),
206
                    ];
207
                    $joinTable = $this->joinTableCache[$classNameForArray]['table'];
208
                    // NB!!!!!!!!!!!!!!
209
                    // local and foreign are swapped here on purpose
210
                    $fieldRelatingToModelExported = $this->joinTableCache[$classNameForArray]['foreign'];
211
                    $fieldRelatingToLookupRelation = $this->joinTableCache[$classNameForArray]['local'];
212
213
                    $limit = Config::inst()->get(static::class, 'limit_to_join_tables');
214
                    $list = DB::query('SELECT "' . $fieldRelatingToModelExported . '", "' . $fieldRelatingToLookupRelation . '" FROM "' . $joinTable . '" LIMIT ' . $limit);
215
                    foreach ($list as $row) {
216
                        if (! isset($this->lookupTableCache[$joinTable][$row[$fieldRelatingToModelExported]])) {
217
                            $this->lookupTableCache[$joinTable][$row[$fieldRelatingToModelExported]] = [];
218
                        }
219
                        $this->lookupTableCache[$joinTable][$row[$fieldRelatingToModelExported]][] = $row[$fieldRelatingToLookupRelation];
220
                    }
221
                } else {
222
                    $joinTable = $this->joinTableCache[$classNameForArray]['table'];
223
                    // NB!!!!!!!!!!!!!!
224
                    // local and foreign are swapped here on purpose
225
                    $fieldRelatingToLookupRelation = $this->joinTableCache[$classNameForArray]['local'];
0 ignored issues
show
Unused Code introduced by
The assignment to $fieldRelatingToLookupRelation is dead and can be removed.
Loading history...
226
                }
227
                if (! empty($this->lookupTableCache[$joinTable][$item->ID])) {
228
                    foreach ($this->lookupTableCache[$joinTable][$item->ID] as $fieldRelatingToLookupRelation) {
229
                        $result[] = $this->lookupTableCache[$classNameForArray][$fieldRelatingToLookupRelation] ?? '';
230
                    }
231
                }
232
            }
233
            return implode($this->exportSeparator, $result);
234
        }
235
    }
236
237
    protected function fieldTypes($fieldName)
238
    {
239
240
        if (count($this->dbCache) === 0) {
241
            $this->dbCache =
242
                Config::inst()->get(static::class, 'db_defaults') +
243
                Config::inst()->get(Member::class, 'db');
244
        }
245
        return $this->dbCache[$fieldName];
246
    }
247
248
    protected function getRelationshipType($methodName)
249
    {
250
        return $this->relCache[$methodName]['type'];
251
    }
252
253
    protected function getRelClassName($methodName)
254
    {
255
        return $this->relCache[$methodName]['class'];
256
    }
257
258
    protected function buildRelCache()
259
    {
260
        if (count($this->relCache) === 0) {
261
262
            foreach (['has_one', 'has_many', 'many_many'] as $relType) {
263
                foreach (Config::inst()->get(Member::class, $relType) as $methodName => $className) {
264
                    $this->relCache[$methodName] = [
265
                        'type' => $relType,
266
                        'class' => $className
267
                    ];
268
                }
269
            }
270
        }
271
    }
272
273
    /**
274
     * Return the columns to export
275
     *
276
     * @param GridField $gridField
277
     *
278
     * @return array
279
     */
280
    protected function getExportColumnsForGridField(GridField $gridField)
281
    {
282
        $modelClass = $gridField->getModelClass();
283
        $custom = Config::inst()->get(static::class, 'custom_exports');
284
        if (isset($custom[$modelClass]) && $custom[$modelClass] === '*') {
285
            $this->exportColumns = AllFields::create($modelClass)->getExportFields();
286
        }
287
        return parent::getExportColumnsForGridField($gridField);
288
    }
289
290
    protected function classToSafeClass(string $class): string
291
    {
292
        return str_replace('\\', '-', $class);
293
    }
294
}
295