sheetPath = $path; $this->sheet = $handle; fwrite($handle, '' . '' . ''); } /** * @param list $cells */ public function appendRow(array $cells): void { if (++$this->row > self::MAX_ROWS) { throw new RuntimeException('导出行数超过 XLSX 单表上限。'); } $xml = ''; foreach (array_values($cells) as $index => $value) { if ($value === null || $value === '') { continue; } $reference = $this->columnName($index + 1) . $this->row; $xml .= is_int($value) || is_float($value) ? '' . $this->number($value) . '' : '' . $this->escape((string) $value) . ''; } fwrite($this->sheet, $xml . ''); } /** * 收尾并写入给定输出流(例如 php://output)。 * * @param resource $output */ public function writeTo($output): void { fwrite($this->sheet, ''); fclose($this->sheet); $zipPath = tempnam(sys_get_temp_dir(), 'xlsx_zip_'); if ($zipPath === false) { $this->cleanup(); throw new RuntimeException('无法创建 XLSX 打包文件。'); } try { $zip = new ZipArchive(); if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { throw new RuntimeException('无法创建 XLSX 压缩包。'); } $zip->addFromString('[Content_Types].xml', $this->contentTypes()); $zip->addFromString('_rels/.rels', $this->rootRelationships()); $zip->addFromString('xl/workbook.xml', $this->workbook()); $zip->addFromString('xl/_rels/workbook.xml.rels', $this->workbookRelationships()); $zip->addFromString('xl/styles.xml', $this->styles()); $zip->addFile($this->sheetPath, 'xl/worksheets/sheet1.xml'); $zip->close(); $handle = fopen($zipPath, 'rb'); if ($handle === false) { throw new RuntimeException('无法读取 XLSX 压缩包。'); } stream_copy_to_stream($handle, $output); fclose($handle); } finally { @unlink($zipPath); $this->cleanup(); } } private function cleanup(): void { if ($this->sheetPath !== '' && is_file($this->sheetPath)) { @unlink($this->sheetPath); } } private function number(int|float $value): string { if (is_int($value)) { return (string) $value; } if (! is_finite($value)) { return '0'; } $text = rtrim(rtrim(sprintf('%.10F', $value), '0'), '.'); return ($text === '' || $text === '-') ? '0' : $text; } private function escape(string $value): string { if (! mb_check_encoding($value, 'UTF-8')) { $value = mb_convert_encoding($value, 'UTF-8', 'UTF-8'); } $value = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/u', '', $value) ?? $value; return htmlspecialchars($value, ENT_QUOTES | ENT_XML1, 'UTF-8'); } private function columnName(int $index): string { $name = ''; while ($index > 0) { $index--; $name = chr(65 + ($index % 26)) . $name; $index = intdiv($index, 26); } return $name; } private function contentTypes(): string { return '' . '' . '' . '' . '' . '' . '' . ''; } private function rootRelationships(): string { return '' . '' . '' . ''; } private function workbook(): string { $name = htmlspecialchars($this->sheetName, ENT_QUOTES | ENT_XML1, 'UTF-8'); return '' . '' . '' . ''; } private function workbookRelationships(): string { return '' . '' . '' . '' . ''; } private function styles(): string { return '' . '' . '' . '' . '' . '' . '' . '' . ''; } }