Passed
Push — master ( 473017...de432a )
by Thomas
03:46
created

ExcelImportExport::excelToAssocArray()   C

Complexity

Conditions 13
Paths 44

Size

Total Lines 58
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 40
c 1
b 0
f 0
dl 0
loc 58
rs 6.6166
cc 13
nc 44
nop 2

How to fix   Long Method    Complexity   

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 LeKoala\ExcelImportExport;
4
5
use Exception;
6
use SilverStripe\Core\ClassInfo;
7
use SilverStripe\ORM\DataObject;
8
use SilverStripe\Core\Config\Config;
9
use PhpOffice\PhpSpreadsheet\IOFactory;
10
use PhpOffice\PhpSpreadsheet\Reader\Csv;
11
use PhpOffice\PhpSpreadsheet\Spreadsheet;
12
use SilverStripe\Core\Config\Configurable;
13
use PhpOffice\PhpSpreadsheet\Reader\IReader;
14
use PhpOffice\PhpSpreadsheet\Shared\Date;
15
16
/**
17
 * Support class for the module
18
 *
19
 * @author Koala
20
 */
21
class ExcelImportExport
22
{
23
    use Configurable;
24
25
    /**
26
     * You boolean want to disable this if you get "No cells exist within the specified range"
27
     * @var bool
28
     */
29
    public static $iterate_only_existing_cells = true;
30
31
    /**
32
     * Some excel sheets need extra processing
33
     * @var boolean
34
     */
35
    public static $process_headers = false;
36
37
    /**
38
     * Get all db fields for a given dataobject class
39
     *
40
     * @param string $class
41
     * @return array
42
     */
43
    public static function allFieldsForClass($class)
44
    {
45
        $dataClasses = ClassInfo::dataClassesFor($class);
46
        $fields      = array();
47
        $dataObjectSchema = DataObject::getSchema();
48
        foreach ($dataClasses as $dataClass) {
49
            $dataFields = $dataObjectSchema->databaseFields($dataClass);
50
            $fields = array_merge($fields, array_keys($dataFields));
51
        }
52
        return array_combine($fields, $fields);
0 ignored issues
show
Bug Best Practice introduced by
The expression return array_combine($fields, $fields) could also return false which is incompatible with the documented return type array. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
53
    }
54
55
    /**
56
     * Get fields that should be exported by default for a class
57
     *
58
     * @param string $class
59
     * @return array
60
     */
61
    public static function exportFieldsForClass($class)
62
    {
63
        $singl = singleton($class);
64
        if ($singl->hasMethod('exportedFields')) {
65
            return $singl->exportedFields();
66
        }
67
        $exportedFields = Config::inst()->get($class, 'exported_fields');
68
69
        if (!$exportedFields) {
70
            $exportedFields = array_keys(self::allFieldsForClass($class));
71
        }
72
73
        $unexportedFields = Config::inst()->get($class, 'unexported_fields');
74
75
        if ($unexportedFields) {
76
            $exportedFields = array_diff($exportedFields, $unexportedFields);
77
        }
78
79
        $fields = [];
80
        foreach ($exportedFields as $key => $value) {
81
            if (is_int($key)) {
82
                $key = $value;
83
            }
84
            $fields[$key] = $value;
85
        }
86
87
        return $fields;
88
    }
89
90
    /**
91
     * Get fields that can be imported by default for a class
92
     *
93
     * @param string $class
94
     * @return array
95
     */
96
    public static function importFieldsForClass($class)
97
    {
98
        $singl = singleton($class);
99
        if ($singl->hasMethod('importedFields')) {
100
            return $singl->importedFields();
101
        }
102
        $importedFields = Config::inst()->get($class, 'imported_fields');
103
104
        if (!$importedFields) {
105
            $importedFields = array_keys(self::allFieldsForClass($class));
106
        }
107
108
        $unimportedFields = Config::inst()->get($class, 'unimported_Fields');
109
110
        if ($unimportedFields) {
111
            $importedFields = array_diff($importedFields, $unimportedFields);
112
        }
113
        return array_combine($importedFields, $importedFields);
0 ignored issues
show
Bug Best Practice introduced by
The expression return array_combine($im...ields, $importedFields) could also return false which is incompatible with the documented return type array. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
114
    }
115
116
    /**
117
     * Output a sample file for a class
118
     *
119
     * A custom file can be provided with a custom sampleExcelFile method
120
     * either as a file path or as a Excel instance
121
     *
122
     * @param string $class
123
     * @return void
124
     */
125
    public static function sampleFileForClass($class)
126
    {
127
        $fileName = "sample-file-for-$class.xlsx";
128
        $spreadsheet    = null;
129
130
        $sng = singleton($class);
131
        if ($sng->hasMethod('sampleExcelFile')) {
132
            $spreadsheet = $sng->sampleExcelFile();
133
134
            // We have a file, output directly
135
            if (is_string($spreadsheet) && is_file($spreadsheet)) {
136
                self::outputHeaders($fileName);
137
                readfile($spreadsheet);
138
                exit();
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
139
            }
140
        }
141
        if (!$spreadsheet) {
142
            $spreadsheet = self::generateDefaultSampleFile($class);
143
        }
144
145
        $writer = self::getDefaultWriter($spreadsheet);
146
        self::outputHeaders($fileName);
147
        $writer->save('php://output');
148
        exit();
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
149
    }
150
151
    public static function getDefaultWriter($spreadsheet)
152
    {
153
        return IOFactory::createWriter($spreadsheet, self::config()->default_writer);
154
    }
155
156
    /**
157
     * Output excel headers
158
     *
159
     * @param string $fileName
160
     * @return void
161
     */
162
    public static function outputHeaders($fileName)
163
    {
164
        $ext = pathinfo($fileName, PATHINFO_EXTENSION);
165
        switch ($ext) {
166
            case 'xlsx':
167
                header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
168
                break;
169
            default:
170
                header('Content-type: application/vnd.ms-excel');
171
                break;
172
        }
173
174
        header('Content-Disposition: attachment; filename="' . $fileName . '"');
175
        header('Cache-Control: max-age=0');
176
        ob_clean();
177
    }
178
179
    /**
180
     * Generate a default import file with all field name
181
     *
182
     * @param string $class
183
     * @return Spreadsheet
184
     */
185
    public static function generateDefaultSampleFile($class)
186
    {
187
        $spreadsheet = new Spreadsheet();
188
        $spreadsheet->getProperties()
189
            ->setCreator('SilverStripe')
190
            ->setTitle("Sample file for $class");
191
        $sheet = $spreadsheet->getActiveSheet();
192
193
        $row = 1;
194
        $col = 1;
195
        $allFields = ExcelImportExport::importFieldsForClass($class);
196
        foreach ($allFields as $header) {
197
            $sheet->setCellValueByColumnAndRow($col, $row, $header);
198
            $col++;
199
        }
200
        return $spreadsheet;
201
    }
202
203
    /**
204
     * Show valid extensions helper (for uploaders)
205
     *
206
     * @return string
207
     */
208
    public static function getValidExtensionsText()
209
    {
210
        return _t(
211
            'ExcelImportExport.VALIDEXTENSIONS',
212
            "Allowed extensions: {extensions}",
213
            array('extensions' => implode(', ', self::getValidExtensions()))
214
        );
215
    }
216
217
    /**
218
     * Extracted from PHPSpreadhseet
219
     *
220
     * @param string $ext
221
     * @return string
222
     */
223
    public static function getReaderForExtension($ext)
224
    {
225
        switch (strtolower($ext)) {
226
            case 'xlsx': // Excel (OfficeOpenXML) Spreadsheet
227
            case 'xlsm': // Excel (OfficeOpenXML) Macro Spreadsheet (macros will be discarded)
228
            case 'xltx': // Excel (OfficeOpenXML) Template
229
            case 'xltm': // Excel (OfficeOpenXML) Macro Template (macros will be discarded)
230
                return 'Xlsx';
231
            case 'xls': // Excel (BIFF) Spreadsheet
232
            case 'xlt': // Excel (BIFF) Template
233
                return 'Xls';
234
            case 'ods': // Open/Libre Offic Calc
235
            case 'ots': // Open/Libre Offic Calc Template
236
                return 'Ods';
237
            case 'slk':
238
                return 'Slk';
239
            case 'xml': // Excel 2003 SpreadSheetML
240
                return 'Xml';
241
            case 'gnumeric':
242
                return 'Gnumeric';
243
            case 'htm':
244
            case 'html':
245
                return 'Html';
246
            case 'csv':
247
                return 'Csv';
248
            default:
249
                throw new Exception("Unsupported file type : $ext");
250
        }
251
    }
252
253
    /**
254
     * Get valid extensions
255
     *
256
     * @return array
257
     */
258
    public static function getValidExtensions()
259
    {
260
        $v = self::config()->allowed_extensions;
261
        if (!$v || !is_array($v)) {
262
            return [];
263
        }
264
        return $v;
265
    }
266
267
    /**
268
     * Save content of an array to a file
269
     *
270
     * @param array $data
271
     * @param string $filepath
272
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
273
     * @return void
274
     */
275
    public static function arrayToFile($data, $filepath)
276
    {
277
        $spreadsheet = new Spreadsheet;
278
        $spreadsheet->setActiveSheetIndex(0);
279
        $spreadsheet->getActiveSheet()->fromArray($data);
280
281
        $ext = pathinfo($filepath, PATHINFO_EXTENSION);
282
283
        // Writer is the same as read : Csv, Xlsx...
284
        $writerType = self::getReaderForExtension($ext);
285
        $writer = IOFactory::createWriter($spreadsheet, $writerType);
286
        $writer->save($filepath);
287
    }
288
289
    /**
290
     * Fast saving to csv
291
     *
292
     * @param array $data
293
     * @param string $filepath
294
     * @param string $delimiter
295
     * @param string $enclosure
296
     * @param string $escapeChar
297
     */
298
    public static function arrayToCsv($data, $filepath, $delimiter = ',', $enclosure = '"', $escapeChar = '\\')
299
    {
300
        if (is_file($filepath)) {
301
            unlink($filepath);
302
        }
303
        $fp = fopen($filepath, 'w');
304
        // UTF 8 fix
305
        fprintf($fp, "\xEF\xBB\xBF");
0 ignored issues
show
Bug introduced by
It seems like $fp can also be of type false; however, parameter $handle of fprintf() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

305
        fprintf(/** @scrutinizer ignore-type */ $fp, "\xEF\xBB\xBF");
Loading history...
306
        foreach ($data as $row) {
307
            fputcsv($fp, $row, $delimiter, $enclosure, $escapeChar);
0 ignored issues
show
Bug introduced by
It seems like $fp can also be of type false; however, parameter $handle of fputcsv() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

307
            fputcsv(/** @scrutinizer ignore-type */ $fp, $row, $delimiter, $enclosure, $escapeChar);
Loading history...
308
        }
309
        return fclose($fp);
0 ignored issues
show
Bug introduced by
It seems like $fp can also be of type false; however, parameter $handle of fclose() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

309
        return fclose(/** @scrutinizer ignore-type */ $fp);
Loading history...
310
    }
311
312
313
    /**
314
     * @param IReader $reader
315
     * @return Csv
316
     */
317
    protected function getCsvReader(IReader $reader)
318
    {
319
        return $reader;
320
    }
321
322
    /**
323
     * Convert a file to an array
324
     *
325
     * @param string $filepath
326
     * @param string $delimiter (csv only)
327
     * @param string $enclosure (csv only)
328
     * @return array
329
     */
330
    public static function fileToArray($filepath, $delimiter = ';', $enclosure = '')
331
    {
332
        $ext = pathinfo($filepath, PATHINFO_EXTENSION);
333
        $readerType = self::getReaderForExtension($ext);
334
        $reader = IOFactory::createReader($readerType);
335
        if ($readerType == 'Csv') {
336
            // @link https://phpspreadsheet.readthedocs.io/en/latest/topics/reading-and-writing-to-file/#setting-csv-options_1
337
            $reader = $this->getCsvReader($reader);
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using $this inside a static method is generally not recommended and can lead to errors in newer PHP versions.
Loading history...
338
            $reader->setDelimiter($delimiter);
339
            $reader->setEnclosure($enclosure);
340
        } else {
341
            // Does not apply to CSV
342
            $reader->setReadDataOnly(true);
343
        }
344
        $data = array();
0 ignored issues
show
Unused Code introduced by
The assignment to $data is dead and can be removed.
Loading history...
345
        if ($reader->canRead($filepath)) {
346
            $excel = $reader->load($filepath);
347
            $data = $excel->getActiveSheet()->toArray(null, true, false, false);
348
        } else {
349
            throw new Exception("Cannot read $filepath");
350
        }
351
        return $data;
352
    }
353
354
    /**
355
     * Convert an excel file to an array
356
     *
357
     * @param string $filepath
358
     * @param string $sheetname Load a specific worksheet by name
359
     * @param true $onlyExisting Avoid reading empty columns
360
     * @return array
361
     */
362
    public static function excelToArray($filepath, $sheetname = null, $onlyExisting = true)
363
    {
364
        $ext = pathinfo($filepath, PATHINFO_EXTENSION);
365
        $readerType = self::getReaderForExtension($ext);
366
        $reader = IOFactory::createReader($readerType);
367
        $reader->setReadDataOnly(true);
368
        if ($sheetname) {
369
            $reader->setLoadSheetsOnly($sheetname);
370
        }
371
        $data = array();
372
        if ($reader->canRead($filepath)) {
373
            $excel = $reader->load($filepath);
374
            if ($onlyExisting) {
0 ignored issues
show
introduced by
The condition $onlyExisting is always true.
Loading history...
375
                $data = [];
376
                foreach ($excel->getActiveSheet()->getRowIterator() as $row) {
377
                    $cellIterator = $row->getCellIterator();
378
                    if (self::$iterate_only_existing_cells) {
379
                        $cellIterator->setIterateOnlyExistingCells(true);
380
                    }
381
                    $cells = [];
382
                    foreach ($cellIterator as $cell) {
383
                        $cells[] = $cell->getFormattedValue();
384
                    }
385
                    $data[] = $cells;
386
                }
387
            } else {
388
                $data = $excel->getActiveSheet()->toArray(null, true, false, false);
389
            }
390
        } else {
391
            throw new Exception("Cannot read $filepath");
392
        }
393
        return $data;
394
    }
395
396
    /**
397
     * Convert an excel file to an associative array
398
     *
399
     * Suppose the first line are the headers of the file
400
     * Headers are trimmed in case you have crappy whitespace in your files
401
     *
402
     * @param string $filepath
403
     * @param string $sheetname Load a specific worksheet by name
404
     * @return array
405
     */
406
    public static function excelToAssocArray($filepath, $sheetname = null)
407
    {
408
        $ext = pathinfo($filepath, PATHINFO_EXTENSION);
409
        $readerType = self::getReaderForExtension($ext);
410
        $reader = IOFactory::createReader($readerType);
411
        $reader->setReadDataOnly(true);
412
        if ($sheetname) {
413
            $reader->setLoadSheetsOnly($sheetname);
414
        }
415
        $data = array();
0 ignored issues
show
Unused Code introduced by
The assignment to $data is dead and can be removed.
Loading history...
416
        if ($reader->canRead($filepath)) {
417
            $excel = $reader->load($filepath);
418
            $data = [];
419
            $headers = [];
420
            $headersCount = 0;
421
            foreach ($excel->getActiveSheet()->getRowIterator() as $row) {
422
                $cellIterator = $row->getCellIterator();
423
                if (self::$iterate_only_existing_cells) {
424
                    $cellIterator->setIterateOnlyExistingCells(true);
425
                }
426
                $cells = [];
427
                foreach ($cellIterator as $cell) {
428
                    $cells[] = $cell->getFormattedValue();
429
                }
430
                if (empty($headers)) {
431
                    $headers = $cells;
432
                    // Some crappy excel file may need this
433
                    if (self::$process_headers) {
434
                        $headers = array_map(function ($v) {
435
                            // Numeric headers are most of the time dates
436
                            if (is_numeric($v)) {
437
                                $v = date('Y-m-d', Date::excelToTimestamp($v));
438
                            }
439
                            // trim does not always work great and headers can contain utf8 stuff
440
                            return is_string($v) ? preg_replace('/(^\s+)|(\s+$)/us', '', $v) : $v;
441
                        }, $headers);
442
                    }
443
                    $headersCount = count($headers);
444
                } else {
445
                    $diff = count($cells) - $headersCount;
446
                    if ($diff != 0) {
447
                        if ($diff > 0) {
448
                            // we have too many cells
449
                            $cells = array_slice($cells, 0, $headersCount);
450
                        } else {
451
                            // we are missing some cells
452
                            for ($i = 0; $i < abs($diff); $i++) {
453
                                $cells[] = null;
454
                            }
455
                        }
456
                    }
457
                    $data[] = array_combine($headers, $cells);
458
                }
459
            }
460
        } else {
461
            throw new Exception("Cannot read $filepath");
462
        }
463
        return $data;
464
    }
465
}
466