ExcelImportExport::fileToArray()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 2
eloc 9
c 2
b 0
f 1
nc 2
nop 4
dl 0
loc 15
rs 9.9666
1
<?php
2
3
namespace LeKoala\ExcelImportExport;
4
5
use Exception;
6
use Generator;
7
use LeKoala\SpreadCompat\Common\Options;
8
use LeKoala\SpreadCompat\SpreadCompat;
0 ignored issues
show
Bug introduced by
The type LeKoala\SpreadCompat\SpreadCompat was not found. Maybe you did not declare it correctly or list all dependencies?

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:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
9
use SilverStripe\Forms\Form;
10
use SilverStripe\Core\ClassInfo;
11
use SilverStripe\ORM\DataObject;
12
use SilverStripe\Control\Controller;
13
use SilverStripe\Core\Config\Config;
14
use SilverStripe\Control\HTTPResponse;
15
use PhpOffice\PhpSpreadsheet\IOFactory;
16
use SilverStripe\Dev\BulkLoader_Result;
17
use PhpOffice\PhpSpreadsheet\Reader\Csv;
18
use PhpOffice\PhpSpreadsheet\Shared\Date;
19
use PhpOffice\PhpSpreadsheet\Spreadsheet;
20
use SilverStripe\Core\Config\Configurable;
21
use PhpOffice\PhpSpreadsheet\Reader\IReader;
22
use PhpOffice\PhpSpreadsheet\Writer\IWriter;
23
use SilverStripe\Assets\FileNameFilter;
24
25
/**
26
 * Support class for the module
27
 *
28
 * @author Koala
29
 */
30
class ExcelImportExport
31
{
32
    use Configurable;
33
34
    /**
35
     * Setting this to false improve performance but may lead to skipped cells
36
     * @var bool
37
     */
38
    public static $iterate_only_existing_cells = false;
39
40
    /**
41
     * Useful if importing only one sheet or if computation fails
42
     * @var bool
43
     */
44
    public static $use_old_calculated_value = false;
45
46
    /**
47
     * Some excel sheets need extra processing
48
     * @var boolean
49
     */
50
    public static $process_headers = false;
51
52
    /**
53
     * @var string
54
     */
55
    public static $default_tmp_reader = 'Xlsx';
56
57
    /**
58
     * @var integer
59
     */
60
    public static $limit_exports = 1000;
61
62
    /**
63
     * You can override the static var with yml config if needed
64
     * @return int
65
     */
66
    public static function getExportLimit()
67
    {
68
        $v = self::config()->export_limit;
69
        if ($v) {
70
            return $v;
71
        }
72
        return self::$limit_exports;
73
    }
74
75
    /**
76
     * Get all db fields for a given dataobject class
77
     *
78
     * @param string $class
79
     * @return array<string,string>
80
     */
81
    public static function allFieldsForClass($class)
82
    {
83
        $dataClasses = ClassInfo::dataClassesFor($class);
84
        $fields      = [];
85
        $dataObjectSchema = DataObject::getSchema();
86
        foreach ($dataClasses as $dataClass) {
87
            $dataFields = $dataObjectSchema->databaseFields($dataClass);
88
            $fields = array_merge($fields, array_keys($dataFields));
89
        }
90
        return array_combine($fields, $fields);
91
    }
92
93
    /**
94
     * Get fields that should be exported by default for a class
95
     *
96
     * @param string $class
97
     * @return array<int|string,mixed>
98
     */
99
    public static function exportFieldsForClass($class)
100
    {
101
        $singl = singleton($class);
102
        if ($singl->hasMethod('exportedFields')) {
103
            return $singl->exportedFields();
104
        }
105
        $exportedFields = Config::inst()->get($class, 'exported_fields');
106
107
        if (!$exportedFields) {
108
            $exportedFields = array_keys(self::allFieldsForClass($class));
109
        }
110
111
        $unexportedFields = Config::inst()->get($class, 'unexported_fields');
112
113
        if ($unexportedFields) {
114
            $exportedFields = array_diff($exportedFields, $unexportedFields);
115
        }
116
117
        $fields = [];
118
        foreach ($exportedFields as $key => $value) {
119
            if (is_int($key)) {
120
                $key = $value;
121
            }
122
            $fields[$key] = $value;
123
        }
124
125
        return $fields;
126
    }
127
128
    /**
129
     * Get fields that can be imported by default for a class
130
     *
131
     * @param string $class
132
     * @return array<string,string>
133
     */
134
    public static function importFieldsForClass($class)
135
    {
136
        $singl = singleton($class);
137
        if ($singl->hasMethod('importedFields')) {
138
            return $singl->importedFields();
139
        }
140
        $importedFields = Config::inst()->get($class, 'imported_fields');
141
142
        if (!$importedFields) {
143
            $importedFields = array_keys(self::allFieldsForClass($class));
144
        }
145
146
        $unimportedFields = Config::inst()->get($class, 'unimported_fields');
147
148
        if ($unimportedFields) {
149
            $importedFields = array_diff($importedFields, $unimportedFields);
150
        }
151
        return array_combine($importedFields, $importedFields);
152
    }
153
154
    public static function createDownloadSampleLink($importer = '')
155
    {
156
        /** @var \SilverStripe\Admin\ModelAdmin $owner */
157
        $owner = Controller::curr();
158
        $class = $owner->getModelClass();
159
        $downloadSampleLink = $owner->Link(str_replace('\\', '-', $class) . '/downloadsample?importer=' . urlencode($importer));
160
        $downloadSample = '<a href="' . $downloadSampleLink . '" class="no-ajax" target="_blank">' . _t(
161
            'ExcelImportExport.DownloadSample',
162
            'Download sample file'
163
        ) . '</a>';
164
        return $downloadSample;
165
    }
166
167
    public static function createSampleFile($data, $fileName)
168
    {
169
        $ext = self::getDefaultExtension();
170
        $fileNameExtension = pathinfo($fileName, PATHINFO_EXTENSION);
171
        if (!$fileNameExtension) {
172
            $fileName .= ".$ext";
173
        }
174
        $options = new Options();
175
        $options->creator = ExcelImportExport::config()->default_creator;
176
        SpreadCompat::output($data, $fileName, $options);
177
        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...
178
    }
179
180
    /**
181
     * Output a sample file for a class
182
     *
183
     * A custom file can be provided with a custom sampleExcelFile method
184
     * either as a file path or as a Excel instance
185
     *
186
     * @param string $class
187
     * @return void
188
     */
189
    public static function sampleFileForClass($class)
190
    {
191
        $ext = self::getDefaultExtension();
192
        $filter = new FileNameFilter();
193
        $fileName = $filter->filter("sample-file-for-$class.$ext");
194
        $spreadsheet = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $spreadsheet is dead and can be removed.
Loading history...
195
196
        $sng = singleton($class);
197
198
        // Deprecated
199
        if ($sng->hasMethod('sampleExcelFile')) {
200
            $spreadsheet = $sng->sampleExcelFile();
201
            // PHPSpreadsheet is required for this
202
            $writer = self::getDefaultWriter($spreadsheet);
203
            self::outputHeaders($fileName);
204
            $writer->save('php://output');
205
            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...
206
        }
207
208
        if ($sng->hasMethod('sampleImportData')) {
209
            $data = $sng->sampleImportData();
210
        } else {
211
            // Simply output the default headers
212
            $data = [ExcelImportExport::importFieldsForClass($class)];
213
        }
214
215
        if (!is_iterable($data)) {
216
            throw new Exception("`sampleImportData` must return an iterable");
217
        }
218
219
        self::createSampleFile($data, $fileName);
220
    }
221
222
    /**
223
     * @param \SilverStripe\Admin\ModelAdmin $controller
224
     * @return bool
225
     */
226
    public static function checkImportForm($controller)
227
    {
228
        if (!$controller->showImportForm) {
229
            return false;
230
        }
231
        $modelClass = $controller->getModelClass();
232
        if (is_array($controller->showImportForm)) {
0 ignored issues
show
introduced by
The condition is_array($controller->showImportForm) is always false.
Loading history...
233
            /** @var array<string> $valid */
234
            $valid = $controller->showImportForm;
235
            if (!in_array($modelClass, $valid)) {
236
                return false;
237
            }
238
        }
239
        return true;
240
    }
241
242
    /**
243
     * This can be used in your ModelAdmin class
244
     *
245
     * public function import(array $data, Form $form): HTTPResponse
246
     * {
247
     *  if (!ExcelImportExport::checkImportForm($this)) {
248
     *      throw new Exception("Invalid import form");
249
     *  }
250
     *  $handler = $data['ImportHandler'] ?? null;
251
     *  if ($handler == "default") {
252
     *      return parent::import($data, $form);
253
     *  }
254
     *  return ExcelImportExport::useCustomHandler($handler, $form, $this);
255
     * }
256
     *
257
     * @param class-string $handler
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
258
     * @param Form $form
259
     * @param Controller $controller
260
     * @return HTTPResponse
261
     */
262
    public static function useCustomHandler($handler, Form $form, Controller $controller)
263
    {
264
        // Check if the class has a ::load method
265
        if (!$handler || !method_exists($handler, "load")) {
266
            $form->sessionMessage("Invalid handler: $handler", 'bad');
267
            return $controller->redirectBack();
268
        }
269
        $file = $_FILES['_CsvFile']['tmp_name'];
270
        $name = $_FILES['_CsvFile']['name'];
271
272
        // Handler could be any class with a ::load method or an instance of BulkLoader
273
        $modelClass = method_exists($handler, 'getModelClass') ? $controller->getModelClass() : null;
274
        $inst = new $handler($modelClass);
275
276
        if (!empty($_POST['OnlyUpdateRecords']) && method_exists($inst, 'setOnlyUpdate')) {
277
            $inst->setOnlyUpdate(true);
278
        }
279
        /** @var ExcelLoaderInterface $inst */
280
        try {
281
            $results = $inst->load($file, $name);
282
        } catch (Exception $e) {
283
            $form->sessionMessage($e->getMessage(), 'bad');
284
            return $controller->redirectBack();
285
        }
286
287
        $message = '';
288
        if ($results instanceof BulkLoader_Result) {
289
            if ($results->CreatedCount()) {
290
                $message .= _t(
291
                    'ModelAdmin.IMPORTEDRECORDS',
292
                    "Imported {count} records.",
293
                    ['count' => $results->CreatedCount()]
294
                );
295
            }
296
            if ($results->UpdatedCount()) {
297
                $message .= _t(
298
                    'ModelAdmin.UPDATEDRECORDS',
299
                    "Updated {count} records.",
300
                    ['count' => $results->UpdatedCount()]
301
                );
302
            }
303
            if ($results->DeletedCount()) {
304
                $message .= _t(
305
                    'ModelAdmin.DELETEDRECORDS',
306
                    "Deleted {count} records.",
307
                    ['count' => $results->DeletedCount()]
308
                );
309
            }
310
            if (!$results->CreatedCount() && !$results->UpdatedCount()) {
311
                $message .= _t('ModelAdmin.NOIMPORT', "Nothing to import");
312
            }
313
        } else {
314
            // Or we have a simple result
315
            $message = $results;
316
        }
317
318
        $form->sessionMessage($message, 'good');
319
        return $controller->redirectBack();
320
    }
321
322
    /**
323
     * @return string
324
     */
325
    public static function getDefaultExtension()
326
    {
327
        return self::config()->default_extension ?? 'xlsx';
328
    }
329
330
    /**
331
     * Get default writer for PHPSpreadsheet if installed
332
     *
333
     * @param Spreadsheet $spreadsheet
334
     * @return IWriter
335
     */
336
    public static function getDefaultWriter(Spreadsheet $spreadsheet): IWriter
337
    {
338
        if (!self::isPhpSpreadsheetAvailable()) {
339
            throw new Exception("PHPSpreadsheet is not installed");
340
        }
341
        $writer = ucfirst(self::getDefaultExtension());
342
        return IOFactory::createWriter($spreadsheet, $writer);
343
    }
344
345
    /**
346
     * Get default reader for PHPSpreadsheet if installed
347
     *
348
     * @return IReader
349
     */
350
    public static function getDefaultReader(): IReader
351
    {
352
        if (!self::isPhpSpreadsheetAvailable()) {
353
            throw new Exception("PHPSpreadsheet is not installed");
354
        }
355
        $writer = ucfirst(self::getDefaultExtension());
356
        return IOFactory::createReader($writer);
357
    }
358
359
    /**
360
     * Output excel headers
361
     *
362
     * @param string $fileName
363
     * @return void
364
     */
365
    public static function outputHeaders($fileName)
366
    {
367
        $ext = pathinfo($fileName, PATHINFO_EXTENSION);
368
        switch ($ext) {
369
            case 'csv':
370
                header('Content-Type: text/csv');
371
                break;
372
            case 'xlsx':
373
                header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
374
                break;
375
            default:
376
                header('Content-type: application/vnd.ms-excel');
377
                break;
378
        }
379
380
        header('Content-Disposition: attachment; filename="' . $fileName . '"');
381
        header('Cache-Control: max-age=0');
382
        ob_clean();
383
    }
384
385
    /**
386
     * Generate a default import file with all field name
387
     * @deprecated
388
     * @param string $class
389
     * @return string
390
     */
391
    public static function generateDefaultSampleFile($class)
392
    {
393
        $opts = [
394
            'creator' => "SilverStripe"
395
        ];
396
        $allFields = ExcelImportExport::importFieldsForClass($class);
397
        $tmpname = SpreadCompat::getTempFilename();
398
        SpreadCompat::write([
399
            $allFields
400
        ], $tmpname, ...$opts);
401
        return $tmpname;
402
    }
403
404
    /**
405
     * Show valid extensions helper (for uploaders)
406
     *
407
     * @return string
408
     */
409
    public static function getValidExtensionsText()
410
    {
411
        return _t(
412
            'ExcelImportExport.VALIDEXTENSIONS',
413
            "Allowed extensions: {extensions}",
414
            array('extensions' => implode(', ', self::getValidExtensions()))
415
        );
416
    }
417
418
    /**
419
     * Extracted from PHPSpreadsheet
420
     *
421
     * @param string $ext
422
     * @return string
423
     */
424
    public static function getReaderForExtension($ext)
425
    {
426
        switch (strtolower($ext)) {
427
            case 'xlsx': // Excel (OfficeOpenXML) Spreadsheet
428
            case 'xlsm': // Excel (OfficeOpenXML) Macro Spreadsheet (macros will be discarded)
429
            case 'xltx': // Excel (OfficeOpenXML) Template
430
            case 'xltm': // Excel (OfficeOpenXML) Macro Template (macros will be discarded)
431
                return 'Xlsx';
432
            case 'xls': // Excel (BIFF) Spreadsheet
433
            case 'xlt': // Excel (BIFF) Template
434
                return 'Xls';
435
            case 'ods': // Open/Libre Offic Calc
436
            case 'ots': // Open/Libre Offic Calc Template
437
                return 'Ods';
438
            case 'slk':
439
                return 'Slk';
440
            case 'xml': // Excel 2003 SpreadSheetML
441
                return 'Xml';
442
            case 'gnumeric':
443
                return 'Gnumeric';
444
            case 'htm':
445
            case 'html':
446
                return 'Html';
447
            case 'csv':
448
                return 'Csv';
449
            case 'tmp': // Useful when importing uploaded files
450
                return self::$default_tmp_reader;
451
            default:
452
                throw new Exception("Unsupported file type : $ext");
453
        }
454
    }
455
456
    /**
457
     * @return boolean
458
     */
459
    public static function isPhpSpreadsheetAvailable()
460
    {
461
        return class_exists(\PhpOffice\PhpSpreadsheet\Spreadsheet::class);
462
    }
463
464
    /**
465
     * If you exported separated files, you can merge them in one big file
466
     * Requires PHPSpreadsheet
467
     * @param array<string> $files
468
     * @return Spreadsheet
469
     */
470
    public static function mergeExcelFiles($files)
471
    {
472
        if (!self::isPhpSpreadsheetAvailable()) {
473
            throw new Exception("PHPSpreadsheet is not installed");
474
        }
475
        $merged = new Spreadsheet;
476
        $merged->removeSheetByIndex(0);
477
        foreach ($files as $filename) {
478
            $remoteExcel = IOFactory::load($filename);
479
            $merged->addExternalSheet($remoteExcel->getActiveSheet());
480
        }
481
        return $merged;
482
    }
483
484
    /**
485
     * Get valid extensions
486
     *
487
     * @return array<string>
488
     */
489
    public static function getValidExtensions()
490
    {
491
        $v = self::config()->allowed_extensions;
492
        if (!$v || !is_array($v)) {
493
            return [];
494
        }
495
        return $v;
496
    }
497
498
    /**
499
     * Save content of an array to a file
500
     *
501
     * @param iterable<array<mixed>> $data
502
     * @param string $filepath
503
     * @return void
504
     */
505
    public static function arrayToFile($data, $filepath)
506
    {
507
        SpreadCompat::write($data, $filepath);
508
    }
509
510
    /**
511
     * Fast saving to csv
512
     *
513
     * @param array<mixed> $data
514
     * @param string $filepath
515
     * @param string $delimiter
516
     * @param string $enclosure
517
     * @param string $escapeChar
518
     * @return void
519
     */
520
    public static function arrayToCsv($data, $filepath, $delimiter = ',', $enclosure = '"', $escapeChar = '\\')
521
    {
522
        if (is_file($filepath)) {
523
            unlink($filepath);
524
        }
525
        $options = new Options();
526
        $options->separator = $delimiter;
527
        $options->enclosure = $enclosure;
528
        $options->escape = $escapeChar;
529
        SpreadCompat::write($data, $filepath, $options);
530
    }
531
532
    public static function excelColumnRange(string $lower = 'A', string $upper = 'ZZ'): Generator
533
    {
534
        ++$upper;
535
        for ($i = $lower; $i !== $upper; ++$i) {
536
            yield $i;
537
        }
538
    }
539
540
    /**
541
     * String from column index.
542
     *
543
     * @param int $index Column index (1 = A)
544
     * @param string $fallback
545
     * @return string
546
     */
547
    public static function getLetter($index, $fallback = 'A')
548
    {
549
        foreach (self::excelColumnRange() as $letter) {
550
            $index--;
551
            if ($index <= 0) {
552
                return $letter;
553
            }
554
        }
555
        return $fallback;
556
    }
557
558
    /**
559
     * Convert a file to an array
560
     *
561
     * @param string $filepath
562
     * @param string $delimiter (csv only)
563
     * @param string $enclosure (csv only)
564
     * @param string $ext if extension cannot be deducted from filepath (eg temp files)
565
     * @return array<mixed>
566
     */
567
    public static function fileToArray($filepath, $delimiter = ';', $enclosure = '', $ext = null)
568
    {
569
        if ($ext === null) {
570
            $ext = pathinfo($filepath, PATHINFO_EXTENSION);
571
        }
572
573
        $data = iterator_to_array(
574
            SpreadCompat::read(
575
                $filepath,
576
                extension: $ext,
577
                separator: $delimiter,
578
                enclosure: $enclosure
579
            )
580
        );
581
        return $data;
582
    }
583
584
    /**
585
     * Convert an excel file to an array
586
     *
587
     * @param string $filepath
588
     * @param string $sheetname Load a specific worksheet by name
589
     * @param bool $onlyExisting Avoid reading empty columns
590
     * @param string $ext if extension cannot be deducted from filepath (eg temp files)
591
     * @return array<mixed>
592
     */
593
    public static function excelToArray($filepath, $sheetname = null, $onlyExisting = true, $ext = null)
594
    {
595
        if (!self::isPhpSpreadsheetAvailable()) {
596
            throw new Exception("PHPSpreadsheet is not installed");
597
        }
598
        if ($ext === null) {
599
            $ext = pathinfo($filepath, PATHINFO_EXTENSION);
0 ignored issues
show
Unused Code introduced by
The assignment to $ext is dead and can be removed.
Loading history...
600
        }
601
        $ext = pathinfo($filepath, PATHINFO_EXTENSION);
602
        $readerType = self::getReaderForExtension($ext);
0 ignored issues
show
Bug introduced by
It seems like $ext can also be of type array; however, parameter $ext of LeKoala\ExcelImportExpor...getReaderForExtension() does only seem to accept string, 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

602
        $readerType = self::getReaderForExtension(/** @scrutinizer ignore-type */ $ext);
Loading history...
603
        $reader = IOFactory::createReader($readerType);
604
        $reader->setReadDataOnly(true);
605
        if ($sheetname) {
606
            $reader->setLoadSheetsOnly($sheetname);
607
        }
608
        $data = [];
609
        if ($reader->canRead($filepath)) {
610
            $excel = $reader->load($filepath);
611
            if ($onlyExisting) {
612
                $data = [];
613
                foreach ($excel->getActiveSheet()->getRowIterator() as $row) {
614
                    $cellIterator = $row->getCellIterator();
615
                    if (self::$iterate_only_existing_cells) {
616
                        $cellIterator->setIterateOnlyExistingCells(true);
617
                    }
618
                    $cells = [];
619
                    foreach ($cellIterator as $cell) {
620
                        if (self::$use_old_calculated_value) {
621
                            $cells[] = $cell->getOldCalculatedValue();
0 ignored issues
show
Bug introduced by
The method getOldCalculatedValue() does not exist on null. ( Ignorable by Annotation )

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

621
                            /** @scrutinizer ignore-call */ 
622
                            $cells[] = $cell->getOldCalculatedValue();

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...
622
                        } else {
623
                            $cells[] = $cell->getFormattedValue();
624
                        }
625
                    }
626
                    $data[] = $cells;
627
                }
628
            } else {
629
                $data = $excel->getActiveSheet()->toArray(null, true, false, false);
630
            }
631
        } else {
632
            throw new Exception("Cannot read $filepath");
633
        }
634
        return $data;
635
    }
636
637
    /**
638
     * @link https://stackoverflow.com/questions/44304795/how-to-retrieve-date-from-table-cell-using-phpspreadsheet#44304796
639
     * @param int|string|null|float $v
640
     * @return string
641
     */
642
    public static function convertExcelDate($v)
643
    {
644
        if ($v === null || !is_numeric($v)) {
645
            return '';
646
        }
647
        if (!self::isPhpSpreadsheetAvailable()) {
648
            throw new Exception("PHPSpreadsheet is not installed");
649
        }
650
        if (is_string($v)) {
651
            $v = intval($v);
652
        }
653
        return date('Y-m-d', Date::excelToTimestamp($v));
654
    }
655
656
    /**
657
     * Convert an excel file to an associative array
658
     *
659
     * Suppose the first line are the headers of the file
660
     * Headers are trimmed in case you have crappy whitespace in your files
661
     *
662
     * @param string $filepath
663
     * @param string $sheetname Load a specific worksheet by name
664
     * @param string $ext if extension cannot be deducted from filepath (eg temp files)
665
     * @return array<mixed>
666
     */
667
    public static function excelToAssocArray($filepath, $sheetname = null, $ext = null)
668
    {
669
        if (!self::isPhpSpreadsheetAvailable()) {
670
            throw new Exception("PHPSpreadsheet is not installed");
671
        }
672
        if ($ext === null) {
673
            $ext = pathinfo($filepath, PATHINFO_EXTENSION);
674
        }
675
        $readerType = self::getReaderForExtension($ext);
0 ignored issues
show
Bug introduced by
It seems like $ext can also be of type array; however, parameter $ext of LeKoala\ExcelImportExpor...getReaderForExtension() does only seem to accept string, 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

675
        $readerType = self::getReaderForExtension(/** @scrutinizer ignore-type */ $ext);
Loading history...
676
        $reader = IOFactory::createReader($readerType);
677
        $reader->setReadDataOnly(true);
678
        if ($sheetname) {
679
            $reader->setLoadSheetsOnly($sheetname);
680
        }
681
        $data = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $data is dead and can be removed.
Loading history...
682
        if ($reader->canRead($filepath)) {
683
            $excel = $reader->load($filepath);
684
            $data = [];
685
            $headers = [];
686
            $headersCount = 0;
687
            foreach ($excel->getActiveSheet()->getRowIterator() as $row) {
688
                $cellIterator = $row->getCellIterator();
689
                if (self::$iterate_only_existing_cells) {
690
                    $cellIterator->setIterateOnlyExistingCells(true);
691
                }
692
                $cells = [];
693
                foreach ($cellIterator as $cell) {
694
                    if (self::$use_old_calculated_value) {
695
                        $cells[] = $cell->getOldCalculatedValue();
696
                    } else {
697
                        $cells[] = $cell->getFormattedValue();
698
                    }
699
                }
700
                if (empty($headers)) {
701
                    $headers = $cells;
702
                    // Some crappy excel file may need this
703
                    if (self::$process_headers) {
704
                        $headers = array_map(function ($v) {
705
                            // Numeric headers are most of the time dates
706
                            if (is_numeric($v)) {
707
                                $v =  self::convertExcelDate($v);
708
                            }
709
                            // trim does not always work great and headers can contain utf8 stuff
710
                            return is_string($v) ? preg_replace('/(^\s+)|(\s+$)/us', '', $v) : $v;
711
                        }, $headers);
712
                    }
713
                    $headersCount = count($headers);
714
                } else {
715
                    $diff = count($cells) - $headersCount;
716
                    if ($diff != 0) {
717
                        if ($diff > 0) {
718
                            // we have too many cells
719
                            $cells = array_slice($cells, 0, $headersCount);
720
                        } else {
721
                            // we are missing some cells
722
                            for ($i = 0; $i < abs($diff); $i++) {
723
                                $cells[] = null;
724
                            }
725
                        }
726
                    }
727
                    $data[] = array_combine($headers, $cells);
728
                }
729
            }
730
        } else {
731
            throw new Exception("Cannot read $filepath");
732
        }
733
        return $data;
734
    }
735
}
736