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 ''
. ''
. ''
. ''
. ''
. ''
. ''
. ''
. '';
}
}