Passed
Push — master ( 6fefb1...57aed0 )
by Nicolaas
08:58
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
24
class ExportAllCustomButton extends GridFieldExportButton
25
{
26
27
    /**
28
     * Example:
29
     *
30
     * ```php
31
     * Member::class => [
32
     *     'Name' => [
33
     *         'FirstName',
34
     *         'Surname',
35
     *         'MyHasOneSalutation.Title',
36
     *     ],
37
     *     'Email' => 'Email',
38
     *     'MyHasOneRelation' => 'MyHasOneRelation.Title',
39
     *     'MyManyManyRelation' => 'MyManyManyRelation.Title',
40
     *     'MyManyManyRelation Nice Title' => 'MyManyManyRelation2.Title',
41
     * ],
42
     * ```
43
     * @var array
44
     */
45
    private static array $custom_exports = [];
46
    private static int $limit_to_lookups = 500;
47
    private static int $limit_to_join_tables = 100000;
48
49
    private static int $max_chars_per_cell = 200;
50
    private static $db_defaults = [
51
        'ID' => 'Int',
52
        'Created' => 'DBDatetime',
53
        'LastEdited' => 'DBDatetime',
54
    ];
55
56
    protected bool $hasCustomExport = false;
57
    protected array $dbCache = [];
58
    protected array $relCache = [];
59
60
    protected array $lookupTableCache = [];
61
62
    protected array $joinTableCache = [];
63
    protected string $exportSeparator = ' ||| ';
64
65
66
    /**
67
     * Generate export fields for CSV.
68
     *
69
     * @param GridField $gridField
70
     *
71
     * @return string
72
     */
73
    public function generateExportFileData($gridField): string
74
    {
75
        $modelClass = $gridField->getModelClass();
76
        $custom = Config::inst()->get(static::class, 'custom_exports');
77
        if (empty($custom[$modelClass])) {
78
            parent::generateExportFileData($gridField);
79
        }
80
81
        // set basic variables
82
        $this->hasCustomExport = true;
83
        $this->exportColumns = $custom[$modelClass];
84
        $this->exportSeparator = ' ' . Config::inst()->get(ExportAllFromModelAdminTraitSettings::class, 'export_separator') . ' ';
85
        $this->buildRelCache();
86
87
        // basics -- see parent::generateExportFileData
88
        $csvWriter = Writer::createFromFileObject(new \SplTempFileObject());
89
        $csvWriter->setDelimiter($this->getCsvSeparator());
90
        $csvWriter->setEnclosure($this->getCsvEnclosure());
91
        $csvWriter->setOutputBOM(Writer::BOM_UTF8);
92
93
        if (!Config::inst()->get(static::class, 'xls_export_disabled')) {
94
            $csvWriter->addFormatter(function (array $row) {
95
                foreach ($row as &$item) {
96
                    // [SS-2017-007] Sanitise XLS executable column values with a leading tab
97
                    if (preg_match('/^[-@=+].*/', $item ?? '')) {
98
                        $item = "\t" . $item;
99
                    }
100
                }
101
                return $row;
102
            });
103
        }
104
105
106
        //Remove GridFieldPaginator as we're going to export the entire list.
107
        $gridField->getConfig()->removeComponentsByType(GridFieldPaginator::class);
108
        $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

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