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
|
|||||
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
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
![]() |
|||||
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 | $limit = Config::inst()->get(static::class, 'limit_to_lookups'); |
||||
180 | if (!isset($this->lookupTableCache[$classNameForArray])) { |
||||
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(),// e.g. MyExportRecordID |
||||
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(); |
||||
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(), //e.g. MyExportRecord_MyManyManyRelationshipNam |
||||
228 | 'local' => $rel->getLocalKey(), // e.g. MyRelationID |
||||
229 | 'foreign' => $rel->getForeignKey(), // e.g. MyExportRecordID |
||||
230 | ]; |
||||
231 | $joinTable = $this->joinTableCache[$relName]['table']; |
||||
232 | // NB!!!!!!!!!!!!!! |
||||
233 | // local and foreign are swapped here on purpose |
||||
234 | $fieldRelatingToModelExported = $this->joinTableCache[$relName]['foreign']; // e.g. MyExportRecordID |
||||
235 | $fieldRelatingToLookupRelation = $this->joinTableCache[$relName]['local'];// e.g. MyRelationID |
||||
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 |
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:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths