Passed
Push — master ( 0e002b...c897ef )
by Thomas
02:27
created

ExcelImportExport::excelToAssocArray()   B

Complexity

Conditions 10
Paths 20

Size

Total Lines 49
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 10
eloc 36
c 3
b 0
f 0
nc 20
nop 2
dl 0
loc 49
rs 7.6666

How to fix   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 SilverStripe\Core\ClassInfo;
6
use SilverStripe\ORM\DataObject;
7
use SilverStripe\Core\Config\Config;
8
use PhpOffice\PhpSpreadsheet\IOFactory;
9
use PhpOffice\PhpSpreadsheet\Spreadsheet;
10
use SilverStripe\Core\Config\Configurable;
11
use Exception;
12
13
/**
14
 * Support class for the module
15
 *
16
 * @author Koala
17
 */
18
class ExcelImportExport
19
{
20
    use Configurable;
21
22
    /**
23
     * Get all db fields for a given dataobject class
24
     *
25
     * @param string $class
26
     * @return array
27
     */
28
    public static function allFieldsForClass($class)
29
    {
30
        $dataClasses = ClassInfo::dataClassesFor($class);
31
        $fields      = array();
32
        $dataObjectSchema = DataObject::getSchema();
33
        foreach ($dataClasses as $dataClass) {
34
            $dataFields = $dataObjectSchema->databaseFields($dataClass);
35
            $fields = array_merge($fields, array_keys($dataFields));
36
        }
37
        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...
38
    }
39
40
    /**
41
     * Get fields that should be exported by default for a class
42
     *
43
     * @param string $class
44
     * @return array
45
     */
46
    public static function exportFieldsForClass($class)
47
    {
48
        $singl = singleton($class);
49
        if ($singl->hasMethod('exportedFields')) {
50
            return $singl->exportedFields();
51
        }
52
        $exportedFields = Config::inst()->get($class, 'exported_fields');
53
54
        if (!$exportedFields) {
55
            $exportedFields = array_keys(self::allFieldsForClass($class));
56
        }
57
58
        $unexportedFields = Config::inst()->get($class, 'unexported_fields');
59
60
        if ($unexportedFields) {
61
            $exportedFields = array_diff($exportedFields, $unexportedFields);
62
        }
63
64
        $fields = [];
65
        foreach ($exportedFields as $key => $value) {
66
            if (is_int($key)) {
67
                $key = $value;
68
            }
69
            $fields[$key] = $value;
70
        }
71
72
        return $fields;
73
    }
74
75
    /**
76
     * Get fields that can be imported by default for a class
77
     *
78
     * @param string $class
79
     * @return array
80
     */
81
    public static function importFieldsForClass($class)
82
    {
83
        $singl = singleton($class);
84
        if ($singl->hasMethod('importedFields')) {
85
            return $singl->importedFields();
86
        }
87
        $importedFields = Config::inst()->get($class, 'imported_fields');
88
89
        if (!$importedFields) {
90
            $importedFields = array_keys(self::allFieldsForClass($class));
91
        }
92
93
        $unimportedFields = Config::inst()->get($class, 'unimported_Fields');
94
95
        if ($unimportedFields) {
96
            $importedFields = array_diff($importedFields, $unimportedFields);
97
        }
98
        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...
99
    }
100
101
    /**
102
     * Output a sample file for a class
103
     *
104
     * A custom file can be provided with a custom sampleExcelFile method
105
     * either as a file path or as a Excel instance
106
     *
107
     * @param string $class
108
     * @return void
109
     */
110
    public static function sampleFileForClass($class)
111
    {
112
        $fileName = "sample-file-for-$class.xlsx";
113
        $spreadsheet    = null;
114
115
        $sng = singleton($class);
116
        if ($sng->hasMethod('sampleExcelFile')) {
117
            $spreadsheet = $sng->sampleExcelFile();
118
119
            // We have a file, output directly
120
            if (is_string($spreadsheet) && is_file($spreadsheet)) {
121
                self::outputHeaders($fileName);
122
                readfile($spreadsheet);
123
                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...
124
            }
125
        }
126
        if (!$spreadsheet) {
127
            $spreadsheet = self::generateDefaultSampleFile($class);
128
        }
129
130
        $writer = self::getDefaultWriter($spreadsheet);
131
        self::outputHeaders($fileName);
132
        $writer->save('php://output');
133
        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...
134
    }
135
136
    public static function getDefaultWriter($spreadsheet)
137
    {
138
        return IOFactory::createWriter($spreadsheet, self::config()->default_writer);
139
    }
140
141
    /**
142
     * Output excel headers
143
     *
144
     * @param string $fileName
145
     * @return void
146
     */
147
    public static function outputHeaders($fileName)
148
    {
149
        $ext = pathinfo($fileName, PATHINFO_EXTENSION);
150
        switch ($ext) {
151
            case 'xlsx':
152
                header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
153
                break;
154
            default:
155
                header('Content-type: application/vnd.ms-excel');
156
                break;
157
        }
158
159
        header('Content-Disposition: attachment; filename="' . $fileName . '"');
160
        header('Cache-Control: max-age=0');
161
        ob_clean();
162
    }
163
164
    /**
165
     * Generate a default import file with all field name
166
     *
167
     * @param string $class
168
     * @return Spreadsheet
169
     */
170
    public static function generateDefaultSampleFile($class)
171
    {
172
        $spreadsheet = new Spreadsheet();
173
        $spreadsheet->getProperties()
174
            ->setCreator('SilverStripe')
175
            ->setTitle("Sample file for $class");
176
        $sheet = $spreadsheet->getActiveSheet();
177
178
        $row = 1;
179
        $col = 1;
180
        $allFields = ExcelImportExport::importFieldsForClass($class);
181
        foreach ($allFields as $header) {
182
            $sheet->setCellValueByColumnAndRow($col, $row, $header);
183
            $col++;
184
        }
185
        return $spreadsheet;
186
    }
187
188
    /**
189
     * Show valid extensions helper (for uploaders)
190
     *
191
     * @return string
192
     */
193
    public static function getValidExtensionsText()
194
    {
195
        return _t(
196
            'ExcelImportExport.VALIDEXTENSIONS',
197
            "Allowed extensions: {extensions}",
198
            array('extensions' => implode(', ', self::getValidExtensions()))
199
        );
200
    }
201
202
    /**
203
     * Extracted from PHPSpreadhseet
204
     *
205
     * @param string $ext
206
     * @return string
207
     */
208
    public static function getReaderForExtension($ext)
209
    {
210
        switch (strtolower($ext)) {
211
            case 'xlsx': // Excel (OfficeOpenXML) Spreadsheet
212
            case 'xlsm': // Excel (OfficeOpenXML) Macro Spreadsheet (macros will be discarded)
213
            case 'xltx': // Excel (OfficeOpenXML) Template
214
            case 'xltm': // Excel (OfficeOpenXML) Macro Template (macros will be discarded)
215
                return 'Xlsx';
216
            case 'xls': // Excel (BIFF) Spreadsheet
217
            case 'xlt': // Excel (BIFF) Template
218
                return 'Xls';
219
            case 'ods': // Open/Libre Offic Calc
220
            case 'ots': // Open/Libre Offic Calc Template
221
                return 'Ods';
222
            case 'slk':
223
                return 'Slk';
224
            case 'xml': // Excel 2003 SpreadSheetML
225
                return 'Xml';
226
            case 'gnumeric':
227
                return 'Gnumeric';
228
            case 'htm':
229
            case 'html':
230
                return 'Html';
231
            case 'csv':
232
                return 'Csv';
233
            default:
234
                throw new Exception("Unsupported file type : $ext");
235
        }
236
    }
237
238
    /**
239
     * Get valid extensions
240
     *
241
     * @return array
242
     */
243
    public static function getValidExtensions()
244
    {
245
        $v = self::config()->allowed_extensions;
246
        if (!$v || !is_array($v)) {
247
            return [];
248
        }
249
        return $v;
250
    }
251
252
    /**
253
     * Convert a file to an array
254
     *
255
     * @param string $filepath
256
     * @param string $delimiter (csv only)
257
     * @param string $enclosure (csv only)
258
     * @return array
259
     */
260
    public static function fileToArray($filepath, $delimiter = ';', $enclosure = '')
261
    {
262
        $ext = pathinfo($filepath, PATHINFO_EXTENSION);
263
        $readerType = self::getReaderForExtension($ext);
264
        $reader = IOFactory::createReader($readerType);
265
        if ($readerType == 'Csv') {
266
            /* @var $reader \PhpOffice\PhpSpreadsheet\Writer\Csv */
267
            // @link https://phpspreadsheet.readthedocs.io/en/latest/topics/reading-and-writing-to-file/#setting-csv-options_1
268
            $reader->setDelimiter($delimiter);
269
            $reader->setEnclosure($enclosure);
270
        } else {
271
            // Does not apply to CSV
272
            $reader->setReadDataOnly(true);
273
        }
274
        $data = array();
0 ignored issues
show
Unused Code introduced by
The assignment to $data is dead and can be removed.
Loading history...
275
        if ($reader->canRead($filepath)) {
0 ignored issues
show
Bug introduced by
The method canRead() does not exist on PhpOffice\PhpSpreadsheet\Writer\Csv. ( Ignorable by Annotation )

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

275
        if ($reader->/** @scrutinizer ignore-call */ canRead($filepath)) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
276
            $excel = $reader->load($filepath);
0 ignored issues
show
Bug introduced by
The method load() does not exist on PhpOffice\PhpSpreadsheet\Writer\Csv. ( Ignorable by Annotation )

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

276
            /** @scrutinizer ignore-call */ 
277
            $excel = $reader->load($filepath);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
277
            $data = $excel->getActiveSheet()->toArray(null, true, false, false);
278
        } else {
279
            throw new Exception("Cannot read $filepath");
280
        }
281
        return $data;
282
    }
283
284
    /**
285
     * Convert an excel file to an array
286
     *
287
     * @param string $filepath
288
     * @param string $sheetname Load a specific worksheet by name
289
     * @param true $onlyExisting Avoid reading empty columns
290
     * @return array
291
     */
292
    public static function excelToArray($filepath, $sheetname = null, $onlyExisting = true)
293
    {
294
        $ext = pathinfo($filepath, PATHINFO_EXTENSION);
295
        $readerType = self::getReaderForExtension($ext);
296
        $reader = IOFactory::createReader($readerType);
297
        $reader->setReadDataOnly(true);
298
        if ($sheetname) {
299
            $reader->setLoadSheetsOnly($sheetname);
300
        }
301
        $data = array();
302
        if ($reader->canRead($filepath)) {
303
            $excel = $reader->load($filepath);
304
            if ($onlyExisting) {
0 ignored issues
show
introduced by
The condition $onlyExisting is always true.
Loading history...
305
                $data = [];
306
                foreach ($excel->getActiveSheet()->getRowIterator() as $row) {
307
                    $cellIterator = $row->getCellIterator();
308
                    $cellIterator->setIterateOnlyExistingCells(true);
309
                    $cells = [];
310
                    foreach ($cellIterator as $cell) {
311
                        $cells[] = $cell->getFormattedValue();
312
                    }
313
                    $data[] = $cells;
314
                }
315
            } else {
316
                $data = $excel->getActiveSheet()->toArray(null, true, false, false);
317
            }
318
        } else {
319
            throw new Exception("Cannot read $filepath");
320
        }
321
        return $data;
322
    }
323
324
    /**
325
     * Convert an excel file to an associative array
326
     *
327
     * Suppose the first line are the headers of the file
328
     * Headers are trimmed in case you have crappy whitespace in your files
329
     *
330
     * @param string $filepath
331
     * @param string $sheetname Load a specific worksheet by name
332
     * @return array
333
     */
334
    public static function excelToAssocArray($filepath, $sheetname = null)
335
    {
336
        $ext = pathinfo($filepath, PATHINFO_EXTENSION);
337
        $readerType = self::getReaderForExtension($ext);
338
        $reader = IOFactory::createReader($readerType);
339
        $reader->setReadDataOnly(true);
340
        if ($sheetname) {
341
            $reader->setLoadSheetsOnly($sheetname);
342
        }
343
        $data = array();
0 ignored issues
show
Unused Code introduced by
The assignment to $data is dead and can be removed.
Loading history...
344
        if ($reader->canRead($filepath)) {
345
            $excel = $reader->load($filepath);
346
            $data = [];
347
            $headers = [];
348
            $headersCount = 0;
349
            foreach ($excel->getActiveSheet()->getRowIterator() as $row) {
350
                $cellIterator = $row->getCellIterator();
351
                $cellIterator->setIterateOnlyExistingCells(true);
352
                $cells = [];
353
                foreach ($cellIterator as $cell) {
354
                    $cells[] = $cell->getFormattedValue();
355
                }
356
                if (empty($headers)) {
357
                    $headers = $cells;
358
                    $headers = array_map(function ($v) {
359
                        // trim does not always work great
360
                        return is_string($v) ? preg_replace('/(^\s+)|(\s+$)/us', '', $v) : $v;
361
                    }, $headers);
362
                    $headersCount = count($headers);
363
                } else {
364
                    $diff = count($cells) - $headersCount;
365
                    if ($diff != 0) {
366
                        if ($diff > 0) {
367
                            // we have too many cells
368
                            $cells = array_slice($cells, 0, $headersCount);
369
                        } else {
370
                            // we are missing some cells
371
                            for ($i = 0; $i < abs($diff); $i++) {
372
                                $cells[] = null;
373
                            }
374
                        }
375
                    }
376
                    $data[] = array_combine($headers, $cells);
377
                }
378
            }
379
        } else {
380
            throw new Exception("Cannot read $filepath");
381
        }
382
        return $data;
383
    }
384
}
385