Completed
Push — master ( c897ef...106c70 )
by Thomas
03:44
created

ExcelImportExport::arrayToFile()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 7
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 12
rs 10
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
     * Save content of an array to a file
254
     *
255
     * @param array $data
256
     * @param string $filepath
257
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
258
     * @return void
259
     */
260
    public static function arrayToFile($data, $filepath)
261
    {
262
        $spreadsheet = new Spreadsheet;
263
        $spreadsheet->setActiveSheetIndex(0);
264
        $spreadsheet->getActiveSheet()->fromArray($data);
265
266
        $ext = pathinfo($filepath, PATHINFO_EXTENSION);
267
268
        // Writer is the same as read : Csv, Xlsx...
269
        $writerType = self::getReaderForExtension($ext);
270
        $writer = IOFactory::createWriter($spreadsheet, $writerType);
271
        $writer->save($filepath);
272
    }
273
274
    /**
275
     * Fast saving to csv
276
     *
277
     * @param array $data
278
     * @param string $filepath
279
     * @param string $delimiter
280
     * @param string $enclosure
281
     * @param string $escapeChar
282
     */
283
    public static function arrayToCsv($data, $filepath, $delimiter = ',', $enclosure = '"', $escapeChar = '\\')
284
    {
285
        if (is_file($filepath)) {
286
            unlink($filepath);
287
        }
288
        $fp = fopen($filepath, 'w');
289
        // UTF 8 fix
290
        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

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

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

294
        return fclose(/** @scrutinizer ignore-type */ $fp);
Loading history...
295
    }
296
297
    /**
298
     * Convert a file to an array
299
     *
300
     * @param string $filepath
301
     * @param string $delimiter (csv only)
302
     * @param string $enclosure (csv only)
303
     * @return array
304
     */
305
    public static function fileToArray($filepath, $delimiter = ';', $enclosure = '')
306
    {
307
        $ext = pathinfo($filepath, PATHINFO_EXTENSION);
308
        $readerType = self::getReaderForExtension($ext);
309
        $reader = IOFactory::createReader($readerType);
310
        if ($readerType == 'Csv') {
311
            /* @var $reader \PhpOffice\PhpSpreadsheet\Writer\Csv */
312
            // @link https://phpspreadsheet.readthedocs.io/en/latest/topics/reading-and-writing-to-file/#setting-csv-options_1
313
            $reader->setDelimiter($delimiter);
314
            $reader->setEnclosure($enclosure);
315
        } else {
316
            // Does not apply to CSV
317
            $reader->setReadDataOnly(true);
318
        }
319
        $data = array();
0 ignored issues
show
Unused Code introduced by
The assignment to $data is dead and can be removed.
Loading history...
320
        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

320
        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...
321
            $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

321
            /** @scrutinizer ignore-call */ 
322
            $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...
322
            $data = $excel->getActiveSheet()->toArray(null, true, false, false);
323
        } else {
324
            throw new Exception("Cannot read $filepath");
325
        }
326
        return $data;
327
    }
328
329
    /**
330
     * Convert an excel file to an array
331
     *
332
     * @param string $filepath
333
     * @param string $sheetname Load a specific worksheet by name
334
     * @param true $onlyExisting Avoid reading empty columns
335
     * @return array
336
     */
337
    public static function excelToArray($filepath, $sheetname = null, $onlyExisting = true)
338
    {
339
        $ext = pathinfo($filepath, PATHINFO_EXTENSION);
340
        $readerType = self::getReaderForExtension($ext);
341
        $reader = IOFactory::createReader($readerType);
342
        $reader->setReadDataOnly(true);
343
        if ($sheetname) {
344
            $reader->setLoadSheetsOnly($sheetname);
345
        }
346
        $data = array();
347
        if ($reader->canRead($filepath)) {
348
            $excel = $reader->load($filepath);
349
            if ($onlyExisting) {
0 ignored issues
show
introduced by
The condition $onlyExisting is always true.
Loading history...
350
                $data = [];
351
                foreach ($excel->getActiveSheet()->getRowIterator() as $row) {
352
                    $cellIterator = $row->getCellIterator();
353
                    $cellIterator->setIterateOnlyExistingCells(true);
354
                    $cells = [];
355
                    foreach ($cellIterator as $cell) {
356
                        $cells[] = $cell->getFormattedValue();
357
                    }
358
                    $data[] = $cells;
359
                }
360
            } else {
361
                $data = $excel->getActiveSheet()->toArray(null, true, false, false);
362
            }
363
        } else {
364
            throw new Exception("Cannot read $filepath");
365
        }
366
        return $data;
367
    }
368
369
    /**
370
     * Convert an excel file to an associative array
371
     *
372
     * Suppose the first line are the headers of the file
373
     * Headers are trimmed in case you have crappy whitespace in your files
374
     *
375
     * @param string $filepath
376
     * @param string $sheetname Load a specific worksheet by name
377
     * @return array
378
     */
379
    public static function excelToAssocArray($filepath, $sheetname = null)
380
    {
381
        $ext = pathinfo($filepath, PATHINFO_EXTENSION);
382
        $readerType = self::getReaderForExtension($ext);
383
        $reader = IOFactory::createReader($readerType);
384
        $reader->setReadDataOnly(true);
385
        if ($sheetname) {
386
            $reader->setLoadSheetsOnly($sheetname);
387
        }
388
        $data = array();
0 ignored issues
show
Unused Code introduced by
The assignment to $data is dead and can be removed.
Loading history...
389
        if ($reader->canRead($filepath)) {
390
            $excel = $reader->load($filepath);
391
            $data = [];
392
            $headers = [];
393
            $headersCount = 0;
394
            foreach ($excel->getActiveSheet()->getRowIterator() as $row) {
395
                $cellIterator = $row->getCellIterator();
396
                $cellIterator->setIterateOnlyExistingCells(true);
397
                $cells = [];
398
                foreach ($cellIterator as $cell) {
399
                    $cells[] = $cell->getFormattedValue();
400
                }
401
                if (empty($headers)) {
402
                    $headers = $cells;
403
                    $headers = array_map(function ($v) {
404
                        // trim does not always work great
405
                        return is_string($v) ? preg_replace('/(^\s+)|(\s+$)/us', '', $v) : $v;
406
                    }, $headers);
407
                    $headersCount = count($headers);
408
                } else {
409
                    $diff = count($cells) - $headersCount;
410
                    if ($diff != 0) {
411
                        if ($diff > 0) {
412
                            // we have too many cells
413
                            $cells = array_slice($cells, 0, $headersCount);
414
                        } else {
415
                            // we are missing some cells
416
                            for ($i = 0; $i < abs($diff); $i++) {
417
                                $cells[] = null;
418
                            }
419
                        }
420
                    }
421
                    $data[] = array_combine($headers, $cells);
422
                }
423
            }
424
        } else {
425
            throw new Exception("Cannot read $filepath");
426
        }
427
        return $data;
428
    }
429
}
430