首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP ZipArchive 建立壓縮檔:多檔打包、資料夾遞歸、記憶體優化完整類別

     編輯:PHP 綜合知識

現代化 ZipBuilder 類別

<?php
declare(strict_types=1);

class ZipBuilder
{
    private ZipArchive $zip;
    private string $tempPath;
    private array $addedFiles = [];

    public function __construct(private string $outputPath)
    {
        $this->tempPath = $outputPath . '.tmp.' . bin2hex(random_bytes(8));
        $this->zip = new ZipArchive();
        
        if ($this->zip->open($this->tempPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
            throw new RuntimeException("無法建立暫存壓縮檔: {$this->tempPath}");
        }
    }

    /**
     * 加入單一檔案
     * @param string $localPath 伺服器本地路徑
     * @param string $zipPath 壓縮包內路徑(相對路徑)
     */
    public function addFile(string $localPath, string $zipPath): self
    {
        if (!is_file($localPath) || !is_readable($localPath)) {
            throw new InvalidArgumentException("檔案不存在或不可讀: $localPath");
        }
        $this->zip->addFile($localPath, $zipPath);
        $this->addedFiles[] = $zipPath;
        return $this;
    }

    /**
     * 遞歸加入整個資料夾
     */
    public function addDirectory(string $localDir, string $zipPrefix = ''): self
    {
        $localDir = rtrim($localDir, '/\\') . DIRECTORY_SEPARATOR;
        $prefix = $zipPrefix ? rtrim($zipPrefix, '/') . '/' : '';
        
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($localDir, FilesystemIterator::SKIP_DOTS),
            RecursiveIteratorIterator::SELF_FIRST
        );

        foreach ($iterator as $item) {
            $relativePath = $prefix . substr($item->getPathname(), strlen($localDir));
            $relativePath = str_replace('\\', '/', $relativePath); // Zip 內統一用正斜線
            
            if ($item->isDir()) {
                $this->zip->addEmptyDir($relativePath);
            } else {
                $this->zip->addFile($item->getPathname(), $relativePath);
            }
            $this->addedFiles[] = $relativePath;
        }
        return $this;
    }

    /**
     * 從字串直接加入內容(無需暫存檔)
     */
    public function addFromString(string $zipPath, string $content): self
    {
        $this->zip->addFromString($zipPath, $content);
        $this->addedFiles[] = $zipPath;
        return $this;
    }

    /**
     * 完成並移動到最終路徑(原子操作)
     */
    public function save(): string
    {
        if ($this->zip->close() !== true) {
            @unlink($this->tempPath);
            throw new RuntimeException('壓縮檔關閉失敗');
        }
        
        // 原子性重命名(同檔案系統下)
        if (!rename($this->tempPath, $this->outputPath)) {
            // 跨裝置備援:複製後刪除
            copy($this->tempPath, $this->outputPath);
            unlink($this->tempPath);
        }
        
        return $this->outputPath;
    }

    public function getFileCount(): int
    {
        return count($this->addedFiles);
    }

    public function __destruct()
    {
        // 防止例外導致暫存檔殘留
        if (isset($this->tempPath) && file_exists($this->tempPath)) {
            @unlink($this->tempPath);
        }
    }
}

使用範例

<?php
try {
    $builder = new ZipBuilder('/var/www/downloads/order_12345.zip');
    
    // 加入單一檔案
    $builder->addFile('/storage/invoices/inv_12345.pdf', 'invoice.pdf');
    
    // 加入圖片資料夾
    $builder->addDirectory('/storage/uploads/order_12345', 'attachments');
    
    // 動態產生 CSV 內容
    $csv = "ID,Name,Price\n1,Product A,100\n2,Product B,200";
    $builder->addFromString('products.csv', $csv);
    
    $finalPath = $builder->save();
    echo "壓縮完成: $finalPath (共 {$builder->getFileCount()} 項)";
    
    // 提供下載
    // header('Content-Type: application/zip');
    // header('Content-Disposition: attachment; filename="order_12345.zip"');
    // readfile($finalPath);
    
} catch (Exception $e) {
    error_log('Zip 建立失敗: ' . $e->getMessage());
    http_response_code(500);
    echo '伺服器錯誤';
}

大檔案/串流輸出(避免佔用磁碟空間)

function streamZip(array $files, string $filename = 'archive.zip'): void
{
    // 需安裝 maennchen/zipstream-php (composer require maennchen/zipstream-php)
    $zip = new \ZipStream\ZipStream($filename);
    
    foreach ($files as $zipPath => $localPath) {
        if (is_file($localPath)) {
            $zip->addFileFromPath($zipPath, $localPath);
        } elseif (is_string($localPath)) { // 視為字串內容
            $zip->addFile($zipPath, $localPath);
        }
    }
    $zip->finish();
    // 直接輸出到瀏覽器,不落地
}

// 使用
streamZip([
    'large_export.csv' => '/data/exports/huge.csv', // 會分塊讀取串流
    'readme.txt' => '這是動態產生的內容',
], 'export.zip');

常見問題排查

  • 中文檔名亂碼:ZipArchive 內部使用 UTF-8,確保 $zipPath 為 UTF-8 編碼;舊版 Windows 解壓軟體可能需 GBK,考慮輸出前轉碼或使用 iconv('UTF-8', 'CP950', $name)。
  • 權限錯誤:執行 PHP 的使用者需對來源檔案有讀取權、對輸出目錄有寫入權。
  • 記憶體不足:大量檔案改用 ZipStream 串流輸出,避免一次載入所有內容。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved