Csv::loadStringOrFile2()   F
last analyzed

Complexity

Conditions 15
Paths 480

Size

Total Lines 74
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 46
CRAP Score 15

Importance

Changes 0
Metric Value
eloc 48
c 0
b 0
f 0
dl 0
loc 74
ccs 46
cts 46
cp 1
rs 2.4722
cc 15
nc 480
nop 3
crap 15

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 PhpOffice\PhpSpreadsheet\Reader;
4
5
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
6
use PhpOffice\PhpSpreadsheet\Cell\Cell;
7
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
8
use PhpOffice\PhpSpreadsheet\Reader\Csv\Delimiter;
9
use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
10
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
11
use PhpOffice\PhpSpreadsheet\Spreadsheet;
12
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
13
use Throwable;
14
15
class Csv extends BaseReader
16
{
17
    const DEFAULT_FALLBACK_ENCODING = 'CP1252';
18
    const GUESS_ENCODING = 'guess';
19
    const UTF8_BOM = "\xEF\xBB\xBF";
20
    const UTF8_BOM_LEN = 3;
21
    const UTF16BE_BOM = "\xfe\xff";
22
    const UTF16BE_BOM_LEN = 2;
23
    const UTF16BE_LF = "\x00\x0a";
24
    const UTF16LE_BOM = "\xff\xfe";
25
    const UTF16LE_BOM_LEN = 2;
26
    const UTF16LE_LF = "\x0a\x00";
27
    const UTF32BE_BOM = "\x00\x00\xfe\xff";
28
    const UTF32BE_BOM_LEN = 4;
29
    const UTF32BE_LF = "\x00\x00\x00\x0a";
30
    const UTF32LE_BOM = "\xff\xfe\x00\x00";
31
    const UTF32LE_BOM_LEN = 4;
32
    const UTF32LE_LF = "\x0a\x00\x00\x00";
33
34
    /**
35
     * Input encoding.
36
     */
37
    private string $inputEncoding = 'UTF-8';
38
39
    /**
40
     * Fallback encoding if guess strikes out.
41
     */
42
    private string $fallbackEncoding = self::DEFAULT_FALLBACK_ENCODING;
43
44
    /**
45
     * Delimiter.
46
     */
47
    private ?string $delimiter = null;
48
49
    /**
50
     * Enclosure.
51
     */
52
    private string $enclosure = '"';
53
54
    /**
55
     * Sheet index to read.
56
     */
57
    private int $sheetIndex = 0;
58
59
    /**
60
     * Load rows contiguously.
61
     */
62
    private bool $contiguous = false;
63
64
    /**
65
     * The character that can escape the enclosure.
66
     * This will probably become unsupported in Php 9.
67
     * Not yet ready to mark deprecated in order to give users
68
     * a migration path.
69
     */
70
    private ?string $escapeCharacter = null;
71
72
    /**
73
     * Callback for setting defaults in construction.
74
     *
75
     * @var ?callable
76
     */
77
    private static $constructorCallback;
78
79
    /** Changed from true to false in release 4.0.0 */
80
    public const DEFAULT_TEST_AUTODETECT = false;
81
82
    /**
83
     * Attempt autodetect line endings (deprecated after PHP8.1)?
84
     */
85
    private bool $testAutodetect = self::DEFAULT_TEST_AUTODETECT;
86
87
    protected bool $castFormattedNumberToNumeric = false;
88
89
    protected bool $preserveNumericFormatting = false;
90
91
    private bool $preserveNullString = false;
92
93
    private bool $sheetNameIsFileName = false;
94
95
    private string $getTrue = 'true';
96
97
    private string $getFalse = 'false';
98
99
    private string $thousandsSeparator = ',';
100
101
    private string $decimalSeparator = '.';
102
103
    /**
104
     * Create a new CSV Reader instance.
105
     */
106 158
    public function __construct()
107
    {
108 158
        parent::__construct();
109 158
        $callback = self::$constructorCallback;
110 158
        if ($callback !== null) {
111 5
            $callback($this);
112
        }
113
    }
114
115
    /**
116
     * Set a callback to change the defaults.
117
     *
118
     * The callback must accept the Csv Reader object as the first parameter,
119
     * and it should return void.
120
     */
121 6
    public static function setConstructorCallback(?callable $callback): void
122
    {
123 6
        self::$constructorCallback = $callback;
124
    }
125
126 1
    public static function getConstructorCallback(): ?callable
127
    {
128 1
        return self::$constructorCallback;
129
    }
130
131 47
    public function setInputEncoding(string $encoding): self
132
    {
133 47
        $this->inputEncoding = $encoding;
134
135 47
        return $this;
136
    }
137
138 1
    public function getInputEncoding(): string
139
    {
140 1
        return $this->inputEncoding;
141
    }
142
143 5
    public function setFallbackEncoding(string $fallbackEncoding): self
144
    {
145 5
        $this->fallbackEncoding = $fallbackEncoding;
146
147 5
        return $this;
148
    }
149
150 1
    public function getFallbackEncoding(): string
151
    {
152 1
        return $this->fallbackEncoding;
153
    }
154
155
    /**
156
     * Move filepointer past any BOM marker.
157
     */
158 134
    protected function skipBOM(): void
159
    {
160 134
        rewind($this->fileHandle);
161
162 134
        if (fgets($this->fileHandle, self::UTF8_BOM_LEN + 1) !== self::UTF8_BOM) {
163 119
            rewind($this->fileHandle);
164
        }
165
    }
166
167
    /**
168
     * Identify any separator that is explicitly set in the file.
169
     */
170 134
    protected function checkSeparator(): void
171
    {
172 134
        $line = fgets($this->fileHandle);
173 134
        if ($line === false) {
174 2
            return;
175
        }
176
177 133
        if ((strlen(trim($line, "\r\n")) == 5) && (stripos($line, 'sep=') === 0)) {
178 3
            $this->delimiter = substr($line, 4, 1);
179
180 3
            return;
181
        }
182
183 131
        $this->skipBOM();
184
    }
185
186
    /**
187
     * Infer the separator if it isn't explicitly set in the file or specified by the user.
188
     */
189 134
    protected function inferSeparator(): void
190
    {
191 134
        $temp = $this->delimiter;
192 134
        if ($temp !== null) {
193 23
            return;
194
        }
195
196 123
        $inferenceEngine = new Delimiter($this->fileHandle, $this->getEscapeCharacter(), $this->enclosure);
197
198
        // If number of lines is 0, nothing to infer : fall back to the default
199 123
        if ($inferenceEngine->linesCounted() === 0) {
200 2
            $this->delimiter = $inferenceEngine->getDefaultDelimiter();
201 2
            $this->skipBOM();
202
203 2
            return;
204
        }
205
206 122
        $this->delimiter = $inferenceEngine->infer();
207
208
        // If no delimiter could be detected, fall back to the default
209 122
        if ($this->delimiter === null) {
210 10
            $this->delimiter = $inferenceEngine->getDefaultDelimiter();
211
        }
212
213 122
        $this->skipBOM();
214
    }
215
216
    /**
217
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
218
     *
219
     * @return array<int, array{worksheetName: string, lastColumnLetter: string, lastColumnIndex: int, totalRows: int, totalColumns: int, sheetState: string}>
220
     */
221 12
    public function listWorksheetInfo(string $filename): array
222
    {
223
        // Open file
224 12
        $this->openFileOrMemory($filename);
225 11
        $fileHandle = $this->fileHandle;
226
227
        // Skip BOM, if any
228 11
        $this->skipBOM();
229 11
        $this->checkSeparator();
230 11
        $this->inferSeparator();
231
232 11
        $worksheetInfo = [
233 11
            [
234 11
                'worksheetName' => 'Worksheet',
235 11
                'lastColumnLetter' => 'A',
236 11
                'lastColumnIndex' => 0,
237 11
                'totalRows' => 0,
238 11
                'totalColumns' => 0,
239 11
            ],
240 11
        ];
241 11
        $delimiter = $this->delimiter ?? '';
242
243
        // Loop through each line of the file in turn
244 11
        $rowData = self::getCsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter);
245 11
        while (is_array($rowData)) {
246 11
            ++$worksheetInfo[0]['totalRows'];
247 11
            $worksheetInfo[0]['lastColumnIndex'] = max($worksheetInfo[0]['lastColumnIndex'], count($rowData) - 1);
248 11
            $rowData = self::getCsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter);
249
        }
250
251 11
        $worksheetInfo[0]['lastColumnLetter'] = Coordinate::stringFromColumnIndex($worksheetInfo[0]['lastColumnIndex'] + 1);
252 11
        $worksheetInfo[0]['totalColumns'] = $worksheetInfo[0]['lastColumnIndex'] + 1;
253 11
        $worksheetInfo[0]['sheetState'] = Worksheet::SHEETSTATE_VISIBLE;
254
255
        // Close file
256 11
        fclose($fileHandle);
257
258 11
        return $worksheetInfo;
259
    }
260
261
    /**
262
     * Loads Spreadsheet from file.
263
     */
264 119
    protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
265
    {
266 119
        $spreadsheet = $this->newSpreadsheet();
267 119
        $spreadsheet->setValueBinder($this->valueBinder);
268
269
        // Load into this instance
270 119
        return $this->loadIntoExisting($filename, $spreadsheet);
271
    }
272
273
    /**
274
     * Loads Spreadsheet from string.
275
     */
276 4
    public function loadSpreadsheetFromString(string $contents): Spreadsheet
277
    {
278 4
        $spreadsheet = $this->newSpreadsheet();
279 4
        $spreadsheet->setValueBinder($this->valueBinder);
280
281
        // Load into this instance
282 4
        return $this->loadStringOrFile('data://text/plain,' . urlencode($contents), $spreadsheet, true);
283
    }
284
285 133
    private function openFileOrMemory(string $filename): void
286
    {
287
        // Open file
288 133
        $fhandle = $this->canRead($filename);
289 133
        if (!$fhandle) {
290 3
            throw new ReaderException($filename . ' is an Invalid Spreadsheet file.');
291
        }
292 130
        if ($this->inputEncoding === 'UTF-8') {
293 90
            $encoding = self::guessEncodingBom($filename);
294 90
            if ($encoding !== '') {
295 6
                $this->inputEncoding = $encoding;
296
            }
297
        }
298 130
        if ($this->inputEncoding === self::GUESS_ENCODING) {
299 18
            $this->inputEncoding = self::guessEncoding($filename, $this->fallbackEncoding);
300
        }
301 130
        $this->openFile($filename);
302 130
        if ($this->inputEncoding !== 'UTF-8') {
303 38
            fclose($this->fileHandle);
304 38
            $entireFile = file_get_contents($filename);
305 38
            $fileHandle = fopen('php://memory', 'r+b');
306 38
            if ($fileHandle !== false && $entireFile !== false) {
307 38
                $this->fileHandle = $fileHandle;
308 38
                $data = StringHelper::convertEncoding($entireFile, 'UTF-8', $this->inputEncoding);
309 38
                fwrite($this->fileHandle, $data);
310 38
                $this->skipBOM();
311
            }
312
        }
313
    }
314
315 4
    public function setTestAutoDetect(bool $value): self
316
    {
317 4
        $this->testAutodetect = $value;
318
319 4
        return $this;
320
    }
321
322 126
    private function setAutoDetect(?string $value, int $version = PHP_VERSION_ID): ?string
323
    {
324 126
        $retVal = null;
325 126
        if ($value !== null && $this->testAutodetect && $version < 90000) {
326 4
            $retVal2 = @ini_set('auto_detect_line_endings', $value);
327 4
            if (is_string($retVal2)) {
328 4
                $retVal = $retVal2;
329
            }
330
        }
331
332 126
        return $retVal;
333
    }
334
335 20
    public function castFormattedNumberToNumeric(
336
        bool $castFormattedNumberToNumeric,
337
        bool $preserveNumericFormatting = false
338
    ): void {
339 20
        $this->castFormattedNumberToNumeric = $castFormattedNumberToNumeric;
340 20
        $this->preserveNumericFormatting = $preserveNumericFormatting;
341
    }
342
343
    /**
344
     * Open data uri for reading.
345
     */
346 4
    private function openDataUri(string $filename): void
347
    {
348 4
        $fileHandle = fopen($filename, 'rb');
349 4
        if ($fileHandle === false) {
350
            // @codeCoverageIgnoreStart
351
            throw new ReaderException('Could not open file ' . $filename . ' for reading.');
352
            // @codeCoverageIgnoreEnd
353
        }
354
355 4
        $this->fileHandle = $fileHandle;
356
    }
357
358
    /**
359
     * Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
360
     */
361 122
    public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet
362
    {
363 122
        return $this->loadStringOrFile($filename, $spreadsheet, false);
364
    }
365
366
    /**
367
     * Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
368
     */
369 126
    private function loadStringOrFile(string $filename, Spreadsheet $spreadsheet, bool $dataUri): Spreadsheet
370
    {
371
        // Deprecated in Php8.1
372 126
        $iniset = $this->setAutoDetect('1');
373
374
        try {
375 126
            $this->loadStringOrFile2($filename, $spreadsheet, $dataUri);
376 124
            $this->setAutoDetect($iniset);
377 2
        } catch (Throwable $e) {
378 2
            $this->setAutoDetect($iniset);
379
380 2
            throw $e;
381
        }
382
383 124
        return $spreadsheet;
384
    }
385
386 126
    private function loadStringOrFile2(string $filename, Spreadsheet $spreadsheet, bool $dataUri): void
387
    {
388
389
        // Open file
390 126
        if ($dataUri) {
391 4
            $this->openDataUri($filename);
392
        } else {
393 122
            $this->openFileOrMemory($filename);
394
        }
395 124
        $fileHandle = $this->fileHandle;
396
397
        // Skip BOM, if any
398 124
        $this->skipBOM();
399 124
        $this->checkSeparator();
400 124
        $this->inferSeparator();
401
402
        // Create new PhpSpreadsheet object
403 124
        while ($spreadsheet->getSheetCount() <= $this->sheetIndex) {
404 4
            $spreadsheet->createSheet();
405
        }
406 124
        $sheet = $spreadsheet->setActiveSheetIndex($this->sheetIndex);
407 124
        if ($this->sheetNameIsFileName) {
408 4
            $sheet->setTitle(substr(basename($filename, '.csv'), 0, Worksheet::SHEET_TITLE_MAXIMUM_LENGTH));
409
        }
410
411
        // Set our starting row based on whether we're in contiguous mode or not
412 124
        $currentRow = 1;
413 124
        $outRow = 0;
414
415
        // Loop through each line of the file in turn
416 124
        $delimiter = $this->delimiter ?? '';
417 124
        $rowData = self::getCsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter);
418 124
        $valueBinder = $this->valueBinder ?? Cell::getValueBinder();
419 124
        $preserveBooleanString = method_exists($valueBinder, 'getBooleanConversion') && $valueBinder->getBooleanConversion();
420 124
        $this->getTrue = Calculation::getTRUE();
421 124
        $this->getFalse = Calculation::getFALSE();
422 124
        $this->thousandsSeparator = StringHelper::getThousandsSeparator();
423 124
        $this->decimalSeparator = StringHelper::getDecimalSeparator();
424 124
        while (is_array($rowData)) {
425 123
            $noOutputYet = true;
426 123
            $columnLetter = 'A';
427 123
            foreach ($rowData as $rowDatum) {
428 123
                if ($preserveBooleanString) {
429 5
                    $rowDatum = $rowDatum ?? '';
430
                } else {
431 119
                    $this->convertBoolean($rowDatum);
432
                }
433 123
                $numberFormatMask = $this->castFormattedNumberToNumeric ? $this->convertFormattedNumber($rowDatum) : '';
434 123
                if (($rowDatum !== '' || $this->preserveNullString) && $this->readFilter->readCell($columnLetter, $currentRow)) {
435 123
                    if ($this->contiguous) {
436 3
                        if ($noOutputYet) {
437 3
                            $noOutputYet = false;
438 3
                            ++$outRow;
439
                        }
440
                    } else {
441 120
                        $outRow = $currentRow;
442
                    }
443
                    // Set basic styling for the value (Note that this could be overloaded by styling in a value binder)
444 123
                    if ($numberFormatMask !== '') {
445 7
                        $sheet->getStyle($columnLetter . $outRow)
446 7
                            ->getNumberFormat()
447 7
                            ->setFormatCode($numberFormatMask);
448
                    }
449
                    // Set cell value
450 123
                    $sheet->getCell($columnLetter . $outRow)->setValue($rowDatum);
451
                }
452 123
                ++$columnLetter;
453
            }
454 123
            $rowData = self::getCsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter);
455 123
            ++$currentRow;
456
        }
457
458
        // Close file
459 124
        fclose($fileHandle);
460
    }
461
462
    /**
463
     * Convert string true/false to boolean, and null to null-string.
464
     */
465 119
    private function convertBoolean(mixed &$rowDatum): void
466
    {
467 119
        if (is_string($rowDatum)) {
468 119
            if (strcasecmp($this->getTrue, $rowDatum) === 0 || strcasecmp('true', $rowDatum) === 0) {
469 12
                $rowDatum = true;
470 119
            } elseif (strcasecmp($this->getFalse, $rowDatum) === 0 || strcasecmp('false', $rowDatum) === 0) {
471 12
                $rowDatum = false;
472
            }
473
        } else {
474
            $rowDatum = $rowDatum ?? '';
475
        }
476
    }
477
478
    /**
479
     * Convert numeric strings to int or float values.
480
     */
481 20
    private function convertFormattedNumber(mixed &$rowDatum): string
482
    {
483 20
        $numberFormatMask = '';
484 20
        if ($this->castFormattedNumberToNumeric === true && is_string($rowDatum)) {
485 20
            $numeric = str_replace(
486 20
                [$this->thousandsSeparator, $this->decimalSeparator],
487 20
                ['', '.'],
488 20
                $rowDatum
489 20
            );
490
491 20
            if (is_numeric($numeric)) {
492 20
                $decimalPos = strpos($rowDatum, $this->decimalSeparator);
493 20
                if ($this->preserveNumericFormatting === true) {
494 7
                    $numberFormatMask = (str_contains($rowDatum, $this->thousandsSeparator))
495 7
                        ? '#,##0' : '0';
496 7
                    if ($decimalPos !== false) {
497 7
                        $decimals = strlen($rowDatum) - $decimalPos - 1;
498 7
                        $numberFormatMask .= '.' . str_repeat('0', min($decimals, 6));
499
                    }
500
                }
501
502 20
                $rowDatum = ($decimalPos !== false) ? (float) $numeric : (int) $numeric;
503
            }
504
        }
505
506 20
        return $numberFormatMask;
507
    }
508
509 14
    public function getDelimiter(): ?string
510
    {
511 14
        return $this->delimiter;
512
    }
513
514 11
    public function setDelimiter(?string $delimiter): self
515
    {
516 11
        $this->delimiter = $delimiter;
517
518 11
        return $this;
519
    }
520
521 2
    public function getEnclosure(): string
522
    {
523 2
        return $this->enclosure;
524
    }
525
526 10
    public function setEnclosure(string $enclosure): self
527
    {
528 10
        if ($enclosure == '') {
529 3
            $enclosure = '"';
530
        }
531 10
        $this->enclosure = $enclosure;
532
533 10
        return $this;
534
    }
535
536 1
    public function getSheetIndex(): int
537
    {
538 1
        return $this->sheetIndex;
539
    }
540
541 5
    public function setSheetIndex(int $indexValue): self
542
    {
543 5
        $this->sheetIndex = $indexValue;
544
545 5
        return $this;
546
    }
547
548 3
    public function setContiguous(bool $contiguous): self
549
    {
550 3
        $this->contiguous = $contiguous;
551
552 3
        return $this;
553
    }
554
555 1
    public function getContiguous(): bool
556
    {
557 1
        return $this->contiguous;
558
    }
559
560
    /**
561
     * Php9 intends to drop support for this parameter in fgetcsv.
562
     * Not yet ready to mark deprecated in order to give users
563
     * a migration path.
564
     */
565 11
    public function setEscapeCharacter(string $escapeCharacter, int $version = PHP_VERSION_ID): self
566
    {
567 11
        if ($version >= 90000 && $escapeCharacter !== '') {
568 2
            throw new ReaderException('Escape character must be null string for Php9+');
569
        }
570
571 9
        $this->escapeCharacter = $escapeCharacter;
572
573 9
        return $this;
574
    }
575
576 123
    public function getEscapeCharacter(int $version = PHP_VERSION_ID): string
577
    {
578 123
        return $this->escapeCharacter ?? self::getDefaultEscapeCharacter($version);
579
    }
580
581
    /**
582
     * Can the current IReader read the file?
583
     */
584 149
    public function canRead(string $filename): bool
585
    {
586
        // Check if file exists
587
        try {
588 149
            $this->openFile($filename);
589 3
        } catch (ReaderException) {
590 3
            return false;
591
        }
592
593 146
        fclose($this->fileHandle);
594
595
        // Trust file extension if any
596 146
        $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
597 146
        if (in_array($extension, ['csv', 'tsv'])) {
598 115
            return true;
599
        }
600
601
        // Attempt to guess mimetype
602 32
        $type = mime_content_type($filename);
603 32
        $supportedTypes = [
604 32
            'application/csv',
605 32
            'text/csv',
606 32
            'text/plain',
607 32
            'inode/x-empty',
608 32
            'text/html',
609 32
        ];
610
611 32
        return in_array($type, $supportedTypes, true);
612
    }
613
614 21
    private static function guessEncodingTestNoBom(string &$encoding, string &$contents, string $compare, string $setEncoding): void
615
    {
616 21
        if ($encoding === '') {
617 21
            $pos = strpos($contents, $compare);
618 21
            if ($pos !== false && $pos % strlen($compare) === 0) {
619 11
                $encoding = $setEncoding;
620
            }
621
        }
622
    }
623
624 21
    private static function guessEncodingNoBom(string $filename): string
625
    {
626 21
        $encoding = '';
627 21
        $contents = (string) file_get_contents($filename);
628 21
        self::guessEncodingTestNoBom($encoding, $contents, self::UTF32BE_LF, 'UTF-32BE');
629 21
        self::guessEncodingTestNoBom($encoding, $contents, self::UTF32LE_LF, 'UTF-32LE');
630 21
        self::guessEncodingTestNoBom($encoding, $contents, self::UTF16BE_LF, 'UTF-16BE');
631 21
        self::guessEncodingTestNoBom($encoding, $contents, self::UTF16LE_LF, 'UTF-16LE');
632 21
        if ($encoding === '' && preg_match('//u', $contents) === 1) {
633 4
            $encoding = 'UTF-8';
634
        }
635
636 21
        return $encoding;
637
    }
638
639 1500
    private static function guessEncodingTestBom(string &$encoding, string $first4, string $compare, string $setEncoding): void
640
    {
641 1500
        if ($encoding === '') {
642 1500
            if (str_starts_with($first4, $compare)) {
643 37
                $encoding = $setEncoding;
644
            }
645
        }
646
    }
647
648 1500
    public static function guessEncodingBom(string $filename, ?string $convertString = null): string
649
    {
650 1500
        $encoding = '';
651 1500
        $first4 = $convertString ?? (string) file_get_contents($filename, false, null, 0, 4);
652 1500
        self::guessEncodingTestBom($encoding, $first4, self::UTF8_BOM, 'UTF-8');
653 1500
        self::guessEncodingTestBom($encoding, $first4, self::UTF16BE_BOM, 'UTF-16BE');
654 1500
        self::guessEncodingTestBom($encoding, $first4, self::UTF32BE_BOM, 'UTF-32BE');
655 1500
        self::guessEncodingTestBom($encoding, $first4, self::UTF32LE_BOM, 'UTF-32LE');
656 1500
        self::guessEncodingTestBom($encoding, $first4, self::UTF16LE_BOM, 'UTF-16LE');
657
658 1500
        return $encoding;
659
    }
660
661 31
    public static function guessEncoding(string $filename, string $dflt = self::DEFAULT_FALLBACK_ENCODING): string
662
    {
663 31
        $encoding = self::guessEncodingBom($filename);
664 31
        if ($encoding === '') {
665 21
            $encoding = self::guessEncodingNoBom($filename);
666
        }
667
668 31
        return ($encoding === '') ? $dflt : $encoding;
669
    }
670
671 1
    public function setPreserveNullString(bool $value): self
672
    {
673 1
        $this->preserveNullString = $value;
674
675 1
        return $this;
676
    }
677
678 1
    public function getPreserveNullString(): bool
679
    {
680 1
        return $this->preserveNullString;
681
    }
682
683 4
    public function setSheetNameIsFileName(bool $sheetNameIsFileName): self
684
    {
685 4
        $this->sheetNameIsFileName = $sheetNameIsFileName;
686
687 4
        return $this;
688
    }
689
690
    /**
691
     * Php8.4 deprecates use of anything other than null string
692
     * as escape Character.
693
     *
694
     * @param resource $stream
695
     * @param null|int<0, max> $length
696
     *
697
     * @return array<int,?string>|false
698
     */
699 134
    private static function getCsv(
700
        $stream,
701
        ?int $length = null,
702
        string $separator = ',',
703
        string $enclosure = '"',
704
        ?string $escape = null,
705
        int $version = PHP_VERSION_ID
706
    ): array|false {
707 134
        $escape = $escape ?? self::getDefaultEscapeCharacter();
708 134
        if ($version >= 80400 && $escape !== '') {
709
            return @fgetcsv($stream, $length, $separator, $enclosure, $escape);
710
        }
711
712 134
        return fgetcsv($stream, $length, $separator, $enclosure, $escape);
713
    }
714
715 2
    public static function affectedByPhp9(
716
        string $filename,
717
        string $inputEncoding = 'UTF-8',
718
        ?string $delimiter = null,
719
        string $enclosure = '"',
720
        string $escapeCharacter = '\\',
721
        int $version = PHP_VERSION_ID
722
    ): bool {
723 2
        if ($version < 70400 || $version >= 90000) {
724 1
            throw new ReaderException('Function valid only for Php7.4 or Php8');
725
        }
726 1
        $reader1 = new self();
727 1
        $reader1->setInputEncoding($inputEncoding)
728 1
            ->setTestAutoDetect(true)
729 1
            ->setEscapeCharacter($escapeCharacter)
730 1
            ->setDelimiter($delimiter)
731 1
            ->setEnclosure($enclosure);
732 1
        $spreadsheet1 = $reader1->load($filename);
733 1
        $sheet1 = $spreadsheet1->getActiveSheet();
734 1
        $array1 = $sheet1->toArray(null, false, false);
735 1
        $spreadsheet1->disconnectWorksheets();
736
737 1
        $reader2 = new self();
738 1
        $reader2->setInputEncoding($inputEncoding)
739 1
            ->setTestAutoDetect(false)
740 1
            ->setEscapeCharacter('')
741 1
            ->setDelimiter($delimiter)
742 1
            ->setEnclosure($enclosure);
743 1
        $spreadsheet2 = $reader2->load($filename);
744 1
        $sheet2 = $spreadsheet2->getActiveSheet();
745 1
        $array2 = $sheet2->toArray(null, false, false);
746 1
        $spreadsheet2->disconnectWorksheets();
747
748 1
        return $array1 !== $array2;
749
    }
750
751
    /**
752
     * The character that will be supplied to fgetcsv
753
     * when escapeCharacter is null.
754
     * It is anticipated that it will conditionally be set
755
     * to null-string for Php9 and above.
756
     */
757 125
    private static function getDefaultEscapeCharacter(int $version = PHP_VERSION_ID): string
758
    {
759 125
        return $version < 90000 ? '\\' : '';
760
    }
761
}
762