Passed
Push — master ( d1836f...473017 )
by Thomas
03:01
created

ExcelImportExport::getCsvReader()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

298
        fprintf(/** @scrutinizer ignore-type */ $fp, "\xEF\xBB\xBF");
Loading history...
299
        foreach ($data as $row) {
300
            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

300
            fputcsv(/** @scrutinizer ignore-type */ $fp, $row, $delimiter, $enclosure, $escapeChar);
Loading history...
301
        }
302
        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

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