Passed
Push — master ( 76b84c...5b6b0f )
by Nicolaas
02:18
created

ExportAllCustomButton::getRelClassName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
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 $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
            $relName = $this->classToSafeClass($item->ClassName) . '_' . $fieldName;
193
            $result = [];
194
            // slow....
195
            if ($relType === 'has_many') {
196
                foreach ($item->$methodName()->column($foreignField) as $val) {
197
                    $result[] = $val;
198
                }
199
                if (!isset($this->joinTableCache[$relName])) {
200
                    // relation object details
201
                    $rel = $item->$methodName();
202
                    $this->joinTableCache[$relName] = [
203
                        'foreign' => $rel->getForeignKey(),
204
                    ];
205
                    // NB!!!!!!!!!!!!!!
206
                    // local and foreign are swapped here on purpose
207
                    $fieldRelatingToModelExported = $this->joinTableCache[$relName]['foreign'];
208
209
                    $list = $className::get()->limit($limit)->map('ID', $fieldRelatingToModelExported)->toArray();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $limit does not seem to be defined for all execution paths leading up to this point.
Loading history...
210
                    foreach ($list as $idOfRelatedItem => $idOfModelExported) {
211
                        if (! isset($this->lookupTableCache[$relName][$idOfModelExported])) {
212
                            $this->lookupTableCache[$relName][$idOfModelExported] = [];
213
                        }
214
                        $this->lookupTableCache[$relName][$idOfModelExported][] = $idOfRelatedItem;
215
                    }
216
                }
217
                if (! empty($this->lookupTableCache[$relName][$item->ID])) {
218
                    foreach ($this->lookupTableCache[$relName][$item->ID] as $fieldRelatingToLookupRelation) {
219
                        $result[] = $this->lookupTableCache[$classNameForArray][$fieldRelatingToLookupRelation] ?? '';
220
                    }
221
                }
222
            } elseif ($relType === 'many_many') {
223
                if (!isset($this->joinTableCache[$relName])) {
224
                    // relation object details
225
                    $rel = $item->$methodName();
226
                    $this->joinTableCache[$relName] = [
227
                        'table' => $rel->getJoinTable(),
228
                        'local' => $rel->getLocalKey(),
229
                        'foreign' => $rel->getForeignKey(),
230
                    ];
231
                    $joinTable = $this->joinTableCache[$relName]['table'];
232
                    // NB!!!!!!!!!!!!!!
233
                    // local and foreign are swapped here on purpose
234
                    $fieldRelatingToModelExported = $this->joinTableCache[$relName]['foreign'];
235
                    $fieldRelatingToLookupRelation = $this->joinTableCache[$relName]['local'];
236
237
                    $limit = Config::inst()->get(static::class, 'limit_to_join_tables');
238
                    $list = DB::query('SELECT "' . $fieldRelatingToModelExported . '", "' . $fieldRelatingToLookupRelation . '" FROM "' . $joinTable . '" LIMIT ' . $limit);
239
                    foreach ($list as $row) {
240
                        if (! isset($this->lookupTableCache[$joinTable][$row[$fieldRelatingToModelExported]])) {
241
                            $this->lookupTableCache[$joinTable][$row[$fieldRelatingToModelExported]] = [];
242
                        }
243
                        $this->lookupTableCache[$joinTable][$row[$fieldRelatingToModelExported]][] = $row[$fieldRelatingToLookupRelation];
244
                    }
245
                } else {
246
                    $joinTable = $this->joinTableCache[$relName]['table'];
247
                }
248
                if (! empty($this->lookupTableCache[$joinTable][$item->ID])) {
249
                    foreach ($this->lookupTableCache[$joinTable][$item->ID] as $fieldRelatingToLookupRelation) {
250
                        $result[] = $this->lookupTableCache[$classNameForArray][$fieldRelatingToLookupRelation] ?? '';
251
                    }
252
                }
253
            }
254
            return implode($this->exportSeparator, $result);
255
        }
256
    }
257
258
    protected function fieldTypes($fieldName)
259
    {
260
261
        if (count($this->dbCache) === 0) {
262
            $this->dbCache =
263
                Config::inst()->get(static::class, 'db_defaults') +
264
                Config::inst()->get(Member::class, 'db');
265
        }
266
        return $this->dbCache[$fieldName];
267
    }
268
269
    protected function getRelationshipType($methodName)
270
    {
271
        return $this->relCache[$methodName]['type'];
272
    }
273
274
    protected function getRelClassName($methodName)
275
    {
276
        return $this->relCache[$methodName]['class'];
277
    }
278
279
    protected function buildRelCache()
280
    {
281
        if (count($this->relCache) === 0) {
282
283
            foreach (['has_one', 'has_many', 'many_many'] as $relType) {
284
                foreach (Config::inst()->get(Member::class, $relType) as $methodName => $className) {
285
                    $this->relCache[$methodName] = [
286
                        'type' => $relType,
287
                        'class' => $className
288
                    ];
289
                }
290
            }
291
        }
292
    }
293
294
    /**
295
     * Return the columns to export
296
     *
297
     * @param GridField $gridField
298
     *
299
     * @return array
300
     */
301
    protected function getExportColumnsForGridField(GridField $gridField)
302
    {
303
        $modelClass = $gridField->getModelClass();
304
        $custom = Config::inst()->get(static::class, 'custom_exports');
305
        if (isset($custom[$modelClass]) && $custom[$modelClass] === '*') {
306
            $this->exportColumns = AllFields::create($modelClass)->getExportFields();
307
        }
308
        return parent::getExportColumnsForGridField($gridField);
309
    }
310
311
    protected function classToSafeClass(string $class): string
312
    {
313
        return str_replace('\\', '-', $class);
314
    }
315
}
316