Completed
Push — master ( 0e8d0b...b4cd42 )
by
unknown
43s queued 34s
created

Csv::loadStringOrFile()   A

Complexity

Conditions 2
Paths 3

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

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