inputEncoding = $encoding; return $this; } public function getInputEncoding(): string { return $this->inputEncoding; } public function setFallbackEncoding(string $fallbackEncoding): self { $this->fallbackEncoding = $fallbackEncoding; return $this; } public function getFallbackEncoding(): string { return $this->fallbackEncoding; } /** * Move filepointer past any BOM marker. */ protected function skipBOM(): void { rewind($this->fileHandle); if (fgets($this->fileHandle, self::UTF8_BOM_LEN + 1) !== self::UTF8_BOM) { rewind($this->fileHandle); } } /** * Identify any separator that is explicitly set in the file. */ protected function checkSeparator(): void { $line = fgets($this->fileHandle); if ($line === false) { return; } if ((strlen(trim($line, "\r\n")) == 5) && (stripos($line, 'sep=') === 0)) { $this->delimiter = substr($line, 4, 1); return; } $this->skipBOM(); } /** * Infer the separator if it isn't explicitly set in the file or specified by the user. */ protected function inferSeparator(): void { $temp = $this->delimiter; if ($temp !== null) { return; } $inferenceEngine = new Delimiter($this->fileHandle, $this->getEscapeCharacter(), $this->enclosure); // If number of lines is 0, nothing to infer : fall back to the default if ($inferenceEngine->linesCounted() === 0) { $this->delimiter = $inferenceEngine->getDefaultDelimiter(); $this->skipBOM(); return; } $this->delimiter = $inferenceEngine->infer(); // If no delimiter could be detected, fall back to the default if ($this->delimiter === null) { $this->delimiter = $inferenceEngine->getDefaultDelimiter(); } $this->skipBOM(); } /** * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns). * * @return array */ public function listWorksheetInfo(string $filename): array { // Open file $this->openFileOrMemory($filename); $fileHandle = $this->fileHandle; // Skip BOM, if any $this->skipBOM(); $this->checkSeparator(); $this->inferSeparator(); $worksheetInfo = [ [ 'worksheetName' => 'Worksheet', 'lastColumnLetter' => 'A', 'lastColumnIndex' => 0, 'totalRows' => 0, 'totalColumns' => 0, ], ]; $delimiter = $this->delimiter ?? ''; // Loop through each line of the file in turn $rowData = self::getCsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter); while (is_array($rowData)) { ++$worksheetInfo[0]['totalRows']; $worksheetInfo[0]['lastColumnIndex'] = max($worksheetInfo[0]['lastColumnIndex'], count($rowData) - 1); $rowData = self::getCsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter); } $worksheetInfo[0]['lastColumnLetter'] = Coordinate::stringFromColumnIndex($worksheetInfo[0]['lastColumnIndex'] + 1); $worksheetInfo[0]['totalColumns'] = $worksheetInfo[0]['lastColumnIndex'] + 1; $worksheetInfo[0]['sheetState'] = Worksheet::SHEETSTATE_VISIBLE; // Close file fclose($fileHandle); return $worksheetInfo; } /** * Loads Spreadsheet from file. */ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet { $spreadsheet = $this->newSpreadsheet(); $spreadsheet->setValueBinder($this->valueBinder); // Load into this instance return $this->loadIntoExisting($filename, $spreadsheet); } /** * Loads Spreadsheet from string. */ public function loadSpreadsheetFromString(string $contents): Spreadsheet { $spreadsheet = $this->newSpreadsheet(); $spreadsheet->setValueBinder($this->valueBinder); // Load into this instance return $this->loadStringOrFile('data://text/plain,' . urlencode($contents), $spreadsheet, true); } private function openFileOrMemory(string $filename): void { // Open file $fhandle = $this->canRead($filename); if (!$fhandle) { throw new ReaderException($filename . ' is an Invalid Spreadsheet file.'); } if ($this->inputEncoding === 'UTF-8') { $encoding = self::guessEncodingBom($filename); if ($encoding !== '') { $this->inputEncoding = $encoding; } } if ($this->inputEncoding === self::GUESS_ENCODING) { $this->inputEncoding = self::guessEncoding($filename, $this->fallbackEncoding); } $this->openFile($filename); if ($this->inputEncoding !== 'UTF-8') { fclose($this->fileHandle); $entireFile = file_get_contents($filename); $fileHandle = fopen('php://memory', 'r+b'); if ($fileHandle !== false && $entireFile !== false) { $this->fileHandle = $fileHandle; $data = StringHelper::convertEncoding($entireFile, 'UTF-8', $this->inputEncoding); fwrite($this->fileHandle, $data); $this->skipBOM(); } } } public function setTestAutoDetect(bool $value): self { $this->testAutodetect = $value; return $this; } private function setAutoDetect(?string $value, int $version = PHP_VERSION_ID): ?string { $retVal = null; if ($value !== null && $this->testAutodetect && $version < 90000) { $retVal2 = @ini_set('auto_detect_line_endings', $value); if (is_string($retVal2)) { $retVal = $retVal2; } } return $retVal; } public function castFormattedNumberToNumeric( bool $castFormattedNumberToNumeric, bool $preserveNumericFormatting = false ): void { $this->castFormattedNumberToNumeric = $castFormattedNumberToNumeric; $this->preserveNumericFormatting = $preserveNumericFormatting; } /** * Open data uri for reading. */ private function openDataUri(string $filename): void { $fileHandle = fopen($filename, 'rb'); if ($fileHandle === false) { // @codeCoverageIgnoreStart throw new ReaderException('Could not open file ' . $filename . ' for reading.'); // @codeCoverageIgnoreEnd } $this->fileHandle = $fileHandle; } /** * Loads PhpSpreadsheet from file into PhpSpreadsheet instance. */ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet { return $this->loadStringOrFile($filename, $spreadsheet, false); } /** * Loads PhpSpreadsheet from file into PhpSpreadsheet instance. */ private function loadStringOrFile(string $filename, Spreadsheet $spreadsheet, bool $dataUri): Spreadsheet { // Deprecated in Php8.1 $iniset = $this->setAutoDetect('1'); try { $this->loadStringOrFile2($filename, $spreadsheet, $dataUri); $this->setAutoDetect($iniset); } catch (Throwable $e) { $this->setAutoDetect($iniset); throw $e; } return $spreadsheet; } private function loadStringOrFile2(string $filename, Spreadsheet $spreadsheet, bool $dataUri): void { // Open file if ($dataUri) { $this->openDataUri($filename); } else { $this->openFileOrMemory($filename); } $fileHandle = $this->fileHandle; // Skip BOM, if any $this->skipBOM(); $this->checkSeparator(); $this->inferSeparator(); // Create new PhpSpreadsheet object while ($spreadsheet->getSheetCount() <= $this->sheetIndex) { $spreadsheet->createSheet(); } $sheet = $spreadsheet->setActiveSheetIndex($this->sheetIndex); if ($this->sheetNameIsFileName) { $sheet->setTitle(substr(basename($filename, '.csv'), 0, Worksheet::SHEET_TITLE_MAXIMUM_LENGTH)); } // Set our starting row based on whether we're in contiguous mode or not $currentRow = 1; $outRow = 0; // Loop through each line of the file in turn $delimiter = $this->delimiter ?? ''; $rowData = self::getCsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter); $valueBinder = $this->valueBinder ?? Cell::getValueBinder(); $preserveBooleanString = method_exists($valueBinder, 'getBooleanConversion') && $valueBinder->getBooleanConversion(); $this->getTrue = Calculation::getTRUE(); $this->getFalse = Calculation::getFALSE(); $this->thousandsSeparator = StringHelper::getThousandsSeparator(); $this->decimalSeparator = StringHelper::getDecimalSeparator(); while (is_array($rowData)) { $noOutputYet = true; $columnLetter = 'A'; foreach ($rowData as $rowDatum) { if ($preserveBooleanString) { $rowDatum = $rowDatum ?? ''; } else { $this->convertBoolean($rowDatum); } $numberFormatMask = $this->castFormattedNumberToNumeric ? $this->convertFormattedNumber($rowDatum) : ''; if (($rowDatum !== '' || $this->preserveNullString) && $this->readFilter->readCell($columnLetter, $currentRow)) { if ($this->contiguous) { if ($noOutputYet) { $noOutputYet = false; ++$outRow; } } else { $outRow = $currentRow; } // Set basic styling for the value (Note that this could be overloaded by styling in a value binder) if ($numberFormatMask !== '') { $sheet->getStyle($columnLetter . $outRow) ->getNumberFormat() ->setFormatCode($numberFormatMask); } // Set cell value $sheet->getCell($columnLetter . $outRow)->setValue($rowDatum); } ++$columnLetter; } $rowData = self::getCsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter); ++$currentRow; } // Close file fclose($fileHandle); } /** * Convert string true/false to boolean, and null to null-string. */ private function convertBoolean(mixed &$rowDatum): void { if (is_string($rowDatum)) { if (strcasecmp($this->getTrue, $rowDatum) === 0 || strcasecmp('true', $rowDatum) === 0) { $rowDatum = true; } elseif (strcasecmp($this->getFalse, $rowDatum) === 0 || strcasecmp('false', $rowDatum) === 0) { $rowDatum = false; } } else { $rowDatum = $rowDatum ?? ''; } } /** * Convert numeric strings to int or float values. */ private function convertFormattedNumber(mixed &$rowDatum): string { $numberFormatMask = ''; if ($this->castFormattedNumberToNumeric === true && is_string($rowDatum)) { $numeric = str_replace( [$this->thousandsSeparator, $this->decimalSeparator], ['', '.'], $rowDatum ); if (is_numeric($numeric)) { $decimalPos = strpos($rowDatum, $this->decimalSeparator); if ($this->preserveNumericFormatting === true) { $numberFormatMask = (str_contains($rowDatum, $this->thousandsSeparator)) ? '#,##0' : '0'; if ($decimalPos !== false) { $decimals = strlen($rowDatum) - $decimalPos - 1; $numberFormatMask .= '.' . str_repeat('0', min($decimals, 6)); } } $rowDatum = ($decimalPos !== false) ? (float) $numeric : (int) $numeric; } } return $numberFormatMask; } public function getDelimiter(): ?string { return $this->delimiter; } public function setDelimiter(?string $delimiter): self { $this->delimiter = $delimiter; return $this; } public function getEnclosure(): string { return $this->enclosure; } public function setEnclosure(string $enclosure): self { if ($enclosure == '') { $enclosure = '"'; } $this->enclosure = $enclosure; return $this; } public function getSheetIndex(): int { return $this->sheetIndex; } public function setSheetIndex(int $indexValue): self { $this->sheetIndex = $indexValue; return $this; } public function setContiguous(bool $contiguous): self { $this->contiguous = $contiguous; return $this; } public function getContiguous(): bool { return $this->contiguous; } /** * Php9 intends to drop support for this parameter in fgetcsv. * Not yet ready to mark deprecated in order to give users * a migration path. */ public function setEscapeCharacter(string $escapeCharacter, int $version = PHP_VERSION_ID): self { if ($version >= 90000 && $escapeCharacter !== '') { throw new ReaderException('Escape character must be null string for Php9+'); } $this->escapeCharacter = $escapeCharacter; return $this; } public function getEscapeCharacter(int $version = PHP_VERSION_ID): string { return $this->escapeCharacter ?? self::getDefaultEscapeCharacter($version); } /** * Can the current IReader read the file? */ public function canRead(string $filename): bool { // Check if file exists try { $this->openFile($filename); } catch (ReaderException) { return false; } fclose($this->fileHandle); // Trust file extension if any $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); if (in_array($extension, ['csv', 'tsv'])) { return true; } // Attempt to guess mimetype $type = mime_content_type($filename); $supportedTypes = [ 'application/csv', 'text/csv', 'text/plain', 'inode/x-empty', 'text/html', ]; return in_array($type, $supportedTypes, true); } private static function guessEncodingTestNoBom(string &$encoding, string &$contents, string $compare, string $setEncoding): void { if ($encoding === '') { $pos = strpos($contents, $compare); if ($pos !== false && $pos % strlen($compare) === 0) { $encoding = $setEncoding; } } } private static function guessEncodingNoBom(string $filename): string { $encoding = ''; $contents = (string) file_get_contents($filename); self::guessEncodingTestNoBom($encoding, $contents, self::UTF32BE_LF, 'UTF-32BE'); self::guessEncodingTestNoBom($encoding, $contents, self::UTF32LE_LF, 'UTF-32LE'); self::guessEncodingTestNoBom($encoding, $contents, self::UTF16BE_LF, 'UTF-16BE'); self::guessEncodingTestNoBom($encoding, $contents, self::UTF16LE_LF, 'UTF-16LE'); if ($encoding === '' && preg_match('//u', $contents) === 1) { $encoding = 'UTF-8'; } return $encoding; } private static function guessEncodingTestBom(string &$encoding, string $first4, string $compare, string $setEncoding): void { if ($encoding === '') { if (str_starts_with($first4, $compare)) { $encoding = $setEncoding; } } } public static function guessEncodingBom(string $filename, ?string $convertString = null): string { $encoding = ''; $first4 = $convertString ?? (string) file_get_contents($filename, false, null, 0, 4); self::guessEncodingTestBom($encoding, $first4, self::UTF8_BOM, 'UTF-8'); self::guessEncodingTestBom($encoding, $first4, self::UTF16BE_BOM, 'UTF-16BE'); self::guessEncodingTestBom($encoding, $first4, self::UTF32BE_BOM, 'UTF-32BE'); self::guessEncodingTestBom($encoding, $first4, self::UTF32LE_BOM, 'UTF-32LE'); self::guessEncodingTestBom($encoding, $first4, self::UTF16LE_BOM, 'UTF-16LE'); return $encoding; } public static function guessEncoding(string $filename, string $dflt = self::DEFAULT_FALLBACK_ENCODING): string { $encoding = self::guessEncodingBom($filename); if ($encoding === '') { $encoding = self::guessEncodingNoBom($filename); } return ($encoding === '') ? $dflt : $encoding; } public function setPreserveNullString(bool $value): self { $this->preserveNullString = $value; return $this; } public function getPreserveNullString(): bool { return $this->preserveNullString; } public function setSheetNameIsFileName(bool $sheetNameIsFileName): self { $this->sheetNameIsFileName = $sheetNameIsFileName; return $this; } /** * Php8.4 deprecates use of anything other than null string * as escape Character. * * @param resource $stream * @param null|int<0, max> $length * * @return array|false */ private static function getCsv( $stream, ?int $length = null, string $separator = ',', string $enclosure = '"', ?string $escape = null, int $version = PHP_VERSION_ID ): array|false { $escape = $escape ?? self::getDefaultEscapeCharacter(); if ($version >= 80400 && $escape !== '') { return @fgetcsv($stream, $length, $separator, $enclosure, $escape); } return fgetcsv($stream, $length, $separator, $enclosure, $escape); } public static function affectedByPhp9( string $filename, string $inputEncoding = 'UTF-8', ?string $delimiter = null, string $enclosure = '"', string $escapeCharacter = '\\', int $version = PHP_VERSION_ID ): bool { if ($version < 70400 || $version >= 90000) { throw new ReaderException('Function valid only for Php7.4 or Php8'); } $reader1 = new self(); $reader1->setInputEncoding($inputEncoding) ->setTestAutoDetect(true) ->setEscapeCharacter($escapeCharacter) ->setDelimiter($delimiter) ->setEnclosure($enclosure); $spreadsheet1 = $reader1->load($filename); $sheet1 = $spreadsheet1->getActiveSheet(); $array1 = $sheet1->toArray(null, false, false); $spreadsheet1->disconnectWorksheets(); $reader2 = new self(); $reader2->setInputEncoding($inputEncoding) ->setTestAutoDetect(false) ->setEscapeCharacter('') ->setDelimiter($delimiter) ->setEnclosure($enclosure); $spreadsheet2 = $reader2->load($filename); $sheet2 = $spreadsheet2->getActiveSheet(); $array2 = $sheet2->toArray(null, false, false); $spreadsheet2->disconnectWorksheets(); return $array1 !== $array2; } /** * The character that will be supplied to fgetcsv * when escapeCharacter is null. * It is anticipated that it will conditionally be set * to null-string for Php9 and above. */ private static function getDefaultEscapeCharacter(int $version = PHP_VERSION_ID): string { return $version < 90000 ? '\\' : ''; } }