Passed
Pull Request — master (#4162)
by Owen
13:05
created

Csv::canRead()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 28
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 3

Importance

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