PHPでZIPファイルを作成・解凍する方法について

採用はこちら

PHPでZIPファイルを作成したり解凍したりする場合は、一般的にZipArchiveクラスを使用します。

ZipArchiveを利用すると、複数ファイルの圧縮、ディレクトリ単位のZIP化、ZIPファイルの展開、特定ファイルだけの解凍などをPHPから実行できます。

Webアプリケーションでファイルをまとめてダウンロードさせたい場合や、アップロードされたZIPファイルを展開したい場合にも利用されます。

目次

PHPでZIPファイルを扱うにはZipArchiveを使用する

ZipArchiveとは

ZipArchiveは、PHPのZIP拡張機能によって提供されるクラスです。

ZIPアーカイブの作成や読み込み、ファイル追加、展開などの操作を行えます。

基本的には、次のようにオブジェクトを作成して使用します。

$zip = new ZipArchive();

代表的なメソッドには次のようなものがあります。

$zip->open();
$zip->addFile();
$zip->addFromString();
$zip->addEmptyDir();
$zip->extractTo();
$zip->close();

ZIPファイルを作成する場合も解凍する場合も、まずopen()でZIPファイルを開くのが基本です。

PHPでZIPファイルを作成する方法

ZipArchiveでZIPファイルを作成する基本例

たとえば、sample.txtimage.jpgarchive.zipにまとめる場合は、次のように記述します。

<?php

$zip = new ZipArchive();

$zipFile = __DIR__ . '/archive.zip';

$result = $zip->open(
    $zipFile,
    ZipArchive::CREATE | ZipArchive::OVERWRITE
);

if ($result !== true) {
    throw new RuntimeException(
        'ZIPファイルを作成できませんでした。'
    );
}

$zip->addFile(
    __DIR__ . '/sample.txt',
    'sample.txt'
);

$zip->addFile(
    __DIR__ . '/image.jpg',
    'image.jpg'
);

$zip->close();

ZipArchive::CREATEは、ZIPファイルが存在しない場合に新しく作成するためのフラグです。

ZipArchive::OVERWRITEは、既存のZIPファイルを新しい内容で作り直したい場合に使用できます。

新規作成と上書きの両方に対応したい場合は、次のように組み合わせます。

ZipArchive::CREATE | ZipArchive::OVERWRITE

なお、open()は成功時にtrueを返します。

失敗時にはエラーコードとなる整数が返されるため、次のように厳密比較するのが適切です。

if ($result !== true) {
    // エラー処理
}

ZIPファイルへファイルを追加する方法

addFile()を使用する

既存のファイルをZIPへ追加する場合は、addFile()を使用します。

$zip->addFile(
    '/var/www/html/files/sample.txt',
    'sample.txt'
);

第1引数にはサーバー上の実際のファイルパスを指定します。

第2引数には、ZIPファイル内部で使用するファイル名を指定します。

たとえば、次のように指定できます。

$zip->addFile(
    '/var/www/html/images/photo.jpg',
    'images/photo.jpg'
);

この場合、ZIP内部では次のような構成になります。

images/
└── photo.jpg

サーバー上の絶対パスをZIP内部へ保存せず、任意のディレクトリ構成に変更できる点がaddFile()の特徴です。

ファイルの存在を確認してから追加する

実際の処理では、ZIPへ追加する前に対象ファイルが存在するか確認しておくと安全です。

$file = __DIR__ . '/sample.txt';

if (is_file($file)) {
    $zip->addFile(
        $file,
        basename($file)
    );
}

basename()を使用すると、フルパスからファイル名だけを取得できます。

たとえば、次のパスであれば、

/var/www/html/files/sample.txt

basename()の結果は、

sample.txt

となります。

複数ファイルをまとめてZIP化する方法

配列とforeachを利用する

複数ファイルをまとめてZIPへ追加したい場合は、配列とforeachを組み合わせると簡単です。

<?php

$files = [
    __DIR__ . '/sample1.txt',
    __DIR__ . '/sample2.txt',
    __DIR__ . '/sample3.txt',
];

$zip = new ZipArchive();

$zipFile = __DIR__ . '/files.zip';

$result = $zip->open(
    $zipFile,
    ZipArchive::CREATE | ZipArchive::OVERWRITE
);

if ($result !== true) {
    throw new RuntimeException(
        'ZIPファイルを作成できませんでした。'
    );
}

foreach ($files as $file) {
    if (!is_file($file)) {
        continue;
    }

    $zip->addFile(
        $file,
        basename($file)
    );
}

$zip->close();

ファイル数が多い場合や、ダウンロード対象を動的に変更する場合にも利用しやすい方法です。

ディレクトリ全体をZIP化する方法

RecursiveDirectoryIteratorを利用する

ディレクトリ内のファイルをサブディレクトリごとZIP化したい場合は、RecursiveDirectoryIteratorRecursiveIteratorIteratorを利用できます。

<?php

$sourceDir = realpath(__DIR__ . '/files');
$zipFile = __DIR__ . '/files.zip';

if ($sourceDir === false) {
    throw new RuntimeException(
        '対象ディレクトリが存在しません。'
    );
}

$zip = new ZipArchive();

$result = $zip->open(
    $zipFile,
    ZipArchive::CREATE | ZipArchive::OVERWRITE
);

if ($result !== true) {
    throw new RuntimeException(
        'ZIPファイルを作成できませんでした。'
    );
}

$iterator = new RecursiveIteratorIterator(
    new RecursiveDirectoryIterator(
        $sourceDir,
        FilesystemIterator::SKIP_DOTS
    ),
    RecursiveIteratorIterator::LEAVES_ONLY
);

foreach ($iterator as $file) {
    if (!$file->isFile()) {
        continue;
    }

    $filePath = $file->getRealPath();

    $relativePath = substr(
        $filePath,
        strlen($sourceDir) + 1
    );

    $zip->addFile(
        $filePath,
        $relativePath
    );
}

$zip->close();

この方法を使えば、サブディレクトリに入っているファイルも含めてZIP化できます。

空ディレクトリはaddEmptyDir()で追加する

addFile()でファイルだけを追加する方法では、ファイルが存在しない空ディレクトリはZIPに含まれません。

空ディレクトリも残したい場合は、addEmptyDir()を使用します。

$zip->addEmptyDir('empty');

ZIP内部には次のようなディレクトリが作成されます。

empty/

ディレクトリ構成そのものを完全に保持したい場合は、ファイルだけでなくディレクトリも走査し、必要に応じてaddEmptyDir()を組み合わせる必要があります。

PHPで生成した文字列をZIPへ直接追加する方法

addFromString()を利用する

PHP上で生成した文字列を、一度ファイルとして保存せずZIPへ格納したい場合はaddFromString()を使用します。

$zip->addFromString(
    'message.txt',
    'Hello ZIP!'
);

CSVやJSONなどを動的に生成してZIPへ格納する場合にも便利です。

たとえばCSVの場合は、次のように記述できます。

$csv = "name,age\n";
$csv .= "Taro,30\n";
$csv .= "Hanako,25\n";

$zip->addFromString(
    'users.csv',
    $csv
);

JSONデータも同じ方法で追加できます。

$data = json_encode(
    [
        'name' => 'Taro',
        'age' => 30,
    ],
    JSON_UNESCAPED_UNICODE
);

$zip->addFromString(
    'data.json',
    $data
);

一時ファイルを作成する必要がないため、動的なデータをZIP化するときに使いやすい方法です。

PHPでZIPファイルを解凍する方法

extractTo()でZIP全体を解凍する

ZIPファイルを展開する場合は、extractTo()を使用します。

<?php

$zipFile = __DIR__ . '/archive.zip';
$extractDir = __DIR__ . '/extract';

$zip = new ZipArchive();

$result = $zip->open($zipFile);

if ($result !== true) {
    throw new RuntimeException(
        'ZIPファイルを開けませんでした。'
    );
}

if (!$zip->extractTo($extractDir)) {
    $zip->close();

    throw new RuntimeException(
        'ZIPファイルを解凍できませんでした。'
    );
}

$zip->close();

extractTo()の第1引数には、解凍先ディレクトリを指定します。

$zip->extractTo(
    __DIR__ . '/extract'
);

成功するとtrue、失敗するとfalseを返します。

そのため、実務では戻り値を確認してエラー処理を行う方が安全です。

ZIP内の特定ファイルだけ解凍する方法

第2引数にファイル名を指定する

extractTo()では、ZIP内のすべてのファイルを展開するだけでなく、特定のファイルだけを取り出すこともできます。

$zip->extractTo(
    __DIR__ . '/extract',
    'sample.txt'
);

これにより、ZIP内部のsample.txtだけが展開されます。

複数ファイルを指定する

複数のファイルを展開したい場合は、配列を指定できます。

$zip->extractTo(
    __DIR__ . '/extract',
    [
        'sample.txt',
        'images/photo.jpg',
    ]
);

ZIP内の一部データだけを利用したい場合に便利です。

ZIPファイルの中身を確認する方法

numFilesとgetNameIndex()を使用する

ZIPを展開する前に、どのようなファイルが含まれているか確認することもできます。

numFilesにはZIP内部のエントリ数が格納されています。

<?php

$zip = new ZipArchive();

if ($zip->open(__DIR__ . '/archive.zip') === true) {
    for ($i = 0; $i < $zip->numFiles; $i++) {
        $filename = $zip->getNameIndex($i);

        echo htmlspecialchars(
            $filename,
            ENT_QUOTES,
            'UTF-8'
        );

        echo '<br>';
    }

    $zip->close();
}

アップロードされたZIPを展開する前にファイル名を確認したり、特定の拡張子だけを処理したりするときにも利用できます。

ZipArchiveが利用できるか確認する方法

class_exists()で確認する

環境によってはZIP拡張機能が有効になっていない場合があります。

次のコードでZipArchiveが利用可能か確認できます。

if (class_exists('ZipArchive')) {
    echo 'ZipArchiveを利用できます。';
} else {
    echo 'ZipArchiveを利用できません。';
}

CLI環境であれば、次のコマンドでも確認できます。

php -m

一覧に、

zip

が表示されていれば、ZIP拡張機能が読み込まれています。

PHPでZIP拡張機能を有効化する方法

Windowsの場合

Windowsでは、使用しているPHPの配布形態やバージョンに応じて、php.iniからZIP拡張機能を有効化します。

実際の設定方法はPHP環境によって異なるため、利用しているPHPのphp.iniと拡張機能ディレクトリを確認する必要があります。

設定変更後は、ApacheやPHP-FPMなど、PHPを実行している環境を再起動します。

その後、

php -m

などでzipが表示されるか確認します。

Linuxの場合

UbuntuやDebian系の環境では、パッケージ管理システムを利用してZIP拡張機能を導入できる場合があります。

たとえば、

sudo apt install php-zip

のようなコマンドが利用されます。

PHPのバージョンを指定して、

sudo apt install php8.4-zip

のようなパッケージを利用する環境もあります。

ただし、正確なパッケージ名や導入方法はOS、PHPバージョン、使用しているリポジトリによって異なります。

Docker公式PHPイメージなどでは別の導入手順になるため、利用環境に合わせて確認することが重要です。

作成したZIPをブラウザからダウンロードさせる方法

ZIP作成後にreadfile()で送信する

Webアプリケーションでは、PHPでZIPファイルを作成したあと、そのままユーザーへダウンロードさせることもできます。

<?php

$zipFile = tempnam(
    sys_get_temp_dir(),
    'zip_'
);

$zip = new ZipArchive();

$result = $zip->open(
    $zipFile,
    ZipArchive::CREATE | ZipArchive::OVERWRITE
);

if ($result !== true) {
    throw new RuntimeException(
        'ZIPファイルを作成できませんでした。'
    );
}

$zip->addFile(
    __DIR__ . '/sample.txt',
    'sample.txt'
);

$zip->close();

try {
    header('Content-Type: application/zip');
    header(
        'Content-Disposition: attachment; filename="download.zip"'
    );
    header(
        'Content-Length: ' . filesize($zipFile)
    );

    readfile($zipFile);
} finally {
    if (is_file($zipFile)) {
        unlink($zipFile);
    }
}

exit;

固定した一時ファイル名を使うより、tempnam()などで一意なファイルを作成した方が、複数ユーザーから同時に処理された場合の競合を防ぎやすくなります。

処理の流れは次のとおりです。

ZIPファイルを作成
↓
HTTPヘッダーを設定
↓
readfile()でZIPを送信
↓
一時ZIPファイルを削除

PHPでZIPファイルを解凍するときの注意点

ユーザーがアップロードしたZIPはそのまま展開しない

自分で作成したZIPではなく、外部ユーザーがアップロードしたZIPを扱う場合は、特に注意が必要です。

単純に、

$zip->extractTo($extractDir);

だけを実行するのではなく、展開前にZIP内部のエントリを確認することが重要です。

PHPやlibzipなどの関連ソフトウェアをサポート対象の最新バージョンへ更新しておくことも重要です。

また、ZIP内のファイル名、ファイル数、ファイルサイズ、ファイル形式なども検証すると、より安全に処理できます。

アプリケーションのディレクトリへ直接解凍しない

ZIP内のファイル名が既存ファイルと重複すると、展開によってファイルが置き換わる可能性があります。

そのため、外部から受け取ったZIPは、アプリケーション本体のディレクトリへ直接展開しない方が安全です。

たとえば、ランダムな一時ディレクトリへ展開します。

/tmp/uploads/ランダムなディレクトリ/

その後、内容を検証してから必要なファイルだけを正式な保存先へ移動する設計にすると安全性を高められます。

ZIP Bombにも注意する

展開後の合計サイズを確認する

ZIPファイル自体の容量が小さくても、解凍すると非常に大きな容量になる場合があります。

このような圧縮ファイルは、サーバーのディスク容量やメモリなどを大量に消費する可能性があります。

いわゆるZIP Bomb対策として、少なくとも次の項目に上限を設定するとよいでしょう。

  • アップロードできるZIP自体の容量
  • ZIP内のファイル数
  • 1ファイルあたりの展開後サイズ
  • ZIP全体の展開後サイズ
  • ディレクトリの階層数

statIndex()などを利用すると、ZIP内部の各エントリに関する情報を取得できます。

可能であれば実際に展開する前に内容を検査し、上限を超えているZIPは処理しない設計にします。

ZIP解凍時のファイル権限にも注意する

展開後のパーミッションを確認する

ZIPを展開したからといって、アーカイブに保存されていた元のファイル権限がそのまま復元されるとは限りません。

Linux環境などでは、アプリケーション側で適切なファイル権限を設計することが重要です。

必要に応じてumask()を利用して、作成されるファイルやディレクトリの権限を制御できます。

$oldUmask = umask(0022);

$zip->extractTo($extractDir);

umask($oldUmask);

特に、解凍したファイルをそのままWeb公開する場合は、実行権限や書き込み権限が必要以上に付与されていないか確認すると安全です。

ZIP処理でエラーが発生した場合の確認方法

getStatusString()を利用する

ZIP操作で問題が発生した場合は、getStatusString()を利用してステータスメッセージを確認できます。

$zip = new ZipArchive();

$result = $zip->open($zipFile);

if ($result !== true) {
    throw new RuntimeException(
        'ZIPを開けませんでした。'
    );
}

ZIPを開いた後の処理でエラー原因を調べたい場合には、

echo $zip->getStatusString();

のように確認できます。

ただし、本番環境では内部エラーの詳細をそのままユーザーへ表示するのではなく、ログへ記録する方が適切です。

PHPでZIPファイルを作成・解凍する方法のまとめ

PHPでZIPファイルを操作するときは、基本的にZipArchiveクラスを利用します。

ZIPを作成する基本的な流れは、

ZipArchiveを生成
↓
open()でZIPを作成
↓
addFile()などでファイルを追加
↓
close()で処理を終了

です。

簡単なコードでは次のようになります。

$zip = new ZipArchive();

$result = $zip->open(
    'archive.zip',
    ZipArchive::CREATE | ZipArchive::OVERWRITE
);

if ($result === true) {
    $zip->addFile(
        'sample.txt',
        'sample.txt'
    );

    $zip->close();
}

ZIPを解凍する場合は、

$zip = new ZipArchive();

if ($zip->open('archive.zip') === true) {
    $zip->extractTo('extract');
    $zip->close();
}

という流れが基本です。

また、addFromString()を使用すればPHPで生成したCSVやJSONを直接ZIPへ格納でき、extractTo()の第2引数を利用すれば特定のファイルだけを展開できます。

一方、外部から受け取ったZIPを解凍する場合は、単純に展開するだけではなく、ZIP内のファイル名やファイル数、展開後サイズなどを事前に確認することが重要です。

ZIP Bombへの対策や既存ファイルの上書き防止、展開用一時ディレクトリの利用、ファイル権限の管理なども含めて設計することで、より安全にPHPでZIPファイルを扱えるようになります。

以上、PHPでZIPファイルを作成・解凍する方法についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次