[PHP]zip_readとは?非推奨のZIPエントリ順次読み込み関数とZipArchiveへの移行を徹底解説

PHP

はじめに

前回の記事では、一連の非推奨手続き型ZIP関数群の起点となる zip_open() を解説しました。今回取り上げる zip_read() は、zip_open() で開いたアーカイブから、エントリ(アーカイブ内の各ファイル)を1つずつ順番に取得していくための関数です。

zip_read() は、while ループと組み合わせて呼び出すことで、アーカイブ内のすべてのエントリを先頭から順番に走査する、という使い方が基本形でした。これまでの記事で紹介してきた zip_entry_name() や zip_entry_filesize() などの関数は、すべてこの zip_read() が返すエントリのリソースを引数として受け取る形で使われていました。前回までと同様、この関数もPHP 7.2.0以降で非推奨とされており、現在は ZipArchive クラスの利用が推奨されています。本記事では、この関数の役割と、ZipArchive によるより柔軟な代替アクセス方法を詳しく解説します。


関数概要

項目内容
関数名zip_read()
所属拡張Zip拡張(手続き型インターフェース)
シグネチャzip_read(resource $zip): resource|false
引数$zip — zip_open() で取得したアーカイブのリソース
戻り値次のエントリを表すリソース。すべて読み終えると false
対応バージョンPHP 4.1.0以降。PHP 7.2.0以降は非推奨
アクセス方式順次アクセス(シーケンシャルアクセス)のみ
推奨される代替ZipArchive::numFiles プロパティと for ループによるランダムアクセス

順次アクセスとランダムアクセスの違い(イメージ図)

  【古い手続き型インターフェース:順次アクセスのみ】

  zip_read($zip) → エントリ1
  zip_read($zip) → エントリ2
  zip_read($zip) → エントリ3
  zip_read($zip) → false(終端)
  ★この記事の対象
  「3番目のエントリだけが欲しい」場合でも、
  1番目・2番目を経由しないとたどり着けない


  【ZipArchive:ランダムアクセスが可能】

  $zip->numFiles        → 3(全体のエントリ数を先に把握できる)
  $zip->getNameIndex(2)  → いきなり3番目(インデックス2)のエントリにアクセス
  $zip->locateName('x')  → 名前から直接インデックスを検索

ポイントは、zip_read() がアーカイブの先頭から一方向にしか進めないという制約を持つ点です。特定のエントリだけを取得したい場合でも、目的のエントリに到達するまで zip_read() を繰り返し呼び出す必要があります。一方、ZipArchive は numFiles プロパティで総数を即座に把握でき、getNameIndex() や locateName() によって任意のエントリへ直接アクセスできます。


実践サンプル7選

例1:非推奨の古い関数を使った基本的な使い方(参考・非推奨)

<?php

class LegacySequentialReader
{
    /**
     * 注意: PHP 7.2.0以降、この一連の関数は非推奨です
     * 新規開発ではZipArchiveの使用を強く推奨します
     */
    public function listAllEntries(string $zipPath): array
    {
        $entries = [];
        $zip = zip_open($zipPath);

        if (is_resource($zip)) {
            // whileループでfalseが返るまで順番に取得する
            while ($entry = zip_read($zip)) {
                $entries[] = zip_entry_name($entry);
            }
            zip_close($zip);
        }

        return $entries;
    }
}

$reader = new LegacySequentialReader();
print_r($reader->listAllEntries('/tmp/sample.zip'));

例2:ZipArchiveを使った推奨の代替実装

<?php

class ModernIndexedReader
{
    /**
     * numFilesプロパティで総数を把握し、
     * forループでインデックスアクセスする
     */
    public function listAllEntries(string $zipPath): array
    {
        $entries = [];
        $zip = new ZipArchive();

        if ($zip->open($zipPath) === true) {
            for ($i = 0; $i < $zip->numFiles; $i++) {
                $entries[] = $zip->getNameIndex($i);
            }
            $zip->close();
        }

        return $entries;
    }
}

$reader = new ModernIndexedReader();
print_r($reader->listAllEntries('/tmp/sample.zip'));

例3:特定のエントリだけを探す処理の非効率さを実演するデモ

<?php

class TargetedSearchComparison
{
    /**
     * 古い方式では、目的のエントリを見つけるまで
     * 先頭から順番に走査する必要がある
     */
    public function findWithLegacyMethod(string $zipPath, string $targetName): ?string
    {
        $zip = zip_open($zipPath);
        $found = null;
        $scannedCount = 0;

        if (is_resource($zip)) {
            while ($entry = zip_read($zip)) {
                $scannedCount++;
                if (zip_entry_name($entry) === $targetName) {
                    $found = zip_entry_name($entry);
                    break;
                }
            }
            zip_close($zip);
        }

        return $found !== null ? "{$found} (走査回数: {$scannedCount})" : null;
    }

    /**
     * ZipArchiveではlocateName()で直接インデックスを取得できる
     */
    public function findWithModernMethod(string $zipPath, string $targetName): ?string
    {
        $zip = new ZipArchive();
        if ($zip->open($zipPath) !== true) {
            return null;
        }

        $index = $zip->locateName($targetName);
        $zip->close();

        return $index !== false ? "{$targetName} (インデックス: {$index}を直接特定)" : null;
    }
}

$comparison = new TargetedSearchComparison();
echo $comparison->findWithLegacyMethod('/tmp/sample.zip', 'readme.txt') . PHP_EOL;
echo $comparison->findWithModernMethod('/tmp/sample.zip', 'readme.txt') . PHP_EOL;

例4:全エントリの総数を事前に把握する処理の比較

<?php

class EntryCountComparison
{
    /**
     * 古い方式ではエントリ数を知るために全件を走査する必要がある
     */
    public function countWithLegacyMethod(string $zipPath): int
    {
        $count = 0;
        $zip = zip_open($zipPath);

        if (is_resource($zip)) {
            while (zip_read($zip)) {
                $count++;
            }
            zip_close($zip);
        }

        return $count;
    }

    /**
     * ZipArchiveではnumFilesプロパティで即座に取得できる
     */
    public function countWithModernMethod(string $zipPath): int
    {
        $zip = new ZipArchive();
        $count = 0;

        if ($zip->open($zipPath) === true) {
            $count = $zip->numFiles;
            $zip->close();
        }

        return $count;
    }
}

$comparison = new EntryCountComparison();
echo 'Legacy: ' . $comparison->countWithLegacyMethod('/tmp/sample.zip') . PHP_EOL;
echo 'Modern: ' . $comparison->countWithModernMethod('/tmp/sample.zip') . PHP_EOL;

例5:条件に合致する複数エントリをまとめて収集するクラス

<?php

class ConditionalEntryCollector
{
    /**
     * ZipArchiveのforループを使って、
     * 特定の条件(拡張子など)に合致するエントリを収集する
     */
    public function collectByExtension(string $zipPath, string $extension): array
    {
        $matched = [];
        $zip = new ZipArchive();

        if ($zip->open($zipPath) === true) {
            for ($i = 0; $i < $zip->numFiles; $i++) {
                $name = $zip->getNameIndex($i);
                if (str_ends_with($name, ".{$extension}")) {
                    $matched[] = $name;
                }
            }
            $zip->close();
        }

        return $matched;
    }
}

$collector = new ConditionalEntryCollector();
print_r($collector->collectByExtension('/tmp/sample.zip', 'php'));

例6:逆順(末尾から)でエントリを処理する実装

<?php

class ReverseOrderProcessor
{
    /**
     * 古い方式では末尾から処理することは不可能だが、
     * ZipArchiveのインデックスアクセスなら簡単に実現できる
     */
    public function processInReverse(string $zipPath): array
    {
        $names = [];
        $zip = new ZipArchive();

        if ($zip->open($zipPath) === true) {
            for ($i = $zip->numFiles - 1; $i >= 0; $i--) {
                $names[] = $zip->getNameIndex($i);
            }
            $zip->close();
        }

        return $names;
    }
}

$processor = new ReverseOrderProcessor();
print_r($processor->processInReverse('/tmp/sample.zip'));

例7:ページネーション(一部のエントリのみ取得)を実現するクラス

<?php

class PaginatedEntryReader
{
    /**
     * 古い方式では途中から読み始めることができないが、
     * ZipArchiveならインデックス範囲を指定して
     * 「ページ単位」でのエントリ取得が容易に実現できる
     */
    public function getPage(string $zipPath, int $page, int $perPage = 10): array
    {
        $entries = [];
        $zip = new ZipArchive();

        if ($zip->open($zipPath) === true) {
            $start = ($page - 1) * $perPage;
            $end = min($start + $perPage, $zip->numFiles);

            for ($i = $start; $i < $end; $i++) {
                $entries[] = $zip->getNameIndex($i);
            }
            $zip->close();
        }

        return $entries;
    }
}

$reader = new PaginatedEntryReader();
print_r($reader->getPage('/tmp/sample.zip', 1, 5));

関連関数との比較

関数/プロパティ役割zip_readとの違い
zip_read()手続き型でエントリを順次取得する本記事の対象。PHP 7.2.0以降は非推奨、順次アクセスのみ
zip_open()手続き型でアーカイブを開くzip_read()の前提となる関数(前々回記事を参照)
ZipArchive::numFilesエントリの総数を保持するプロパティ事前に総数を即座に把握できる
ZipArchive::getNameIndex()インデックス指定でエントリ名を取得任意の位置に直接アクセスできる(ランダムアクセス)
ZipArchive::locateName()名前からインデックスを検索特定のエントリを効率的に探す際に使う

よくある落とし穴(注意点)

  1. PHP 7.2.0以降でE_DEPRECATED警告が発生する これまでの記事と同様、この関数を使用すると非推奨警告が発生します。早期の ZipArchive への移行が推奨されます。
  2. 特定のエントリを探す処理が非効率になりがちだった zip_read() は先頭からしか走査できないため、目的のエントリが末尾に近い場合、それまでのすべてのエントリを無駄に読み進める必要がありました。ZipArchive::locateName() を使えば、この非効率さを解消できます(例3を参照)。
  3. エントリ総数の把握にも全件走査が必要だった 単に「アーカイブに何個のファイルが入っているか」を知りたいだけでも、zip_read() ではループを最後まで回す必要がありました。ZipArchive::numFiles プロパティなら、開いた瞬間に即座に把握できます(例4を参照)。
  4. 逆順処理やページネーションのような柔軟なアクセスができない zip_read() は一方向の順次アクセスしかサポートしないため、末尾から処理したり、特定範囲だけを取得したりするような柔軟な操作は実現困難でした。ZipArchive のインデックスベースのアクセスなら、こうした要件にも簡単に対応できます(例6・例7を参照)。
  5. whileループの条件式に注意する while ($entry = zip_read($zip)) という書き方は一般的ですが、エントリ名が空文字列など、真偽値として偽に評価される特殊なケースがあった場合に、意図せずループが終了してしまう可能性も歴史的には指摘されていました。ZipArchive の for ループによるインデックスベースの反復は、このような曖昧さを排除できます。

まとめ

観点まとめ
何をする関数かzip_open()で開いたアーカイブから、エントリを1つずつ順番に取得する
現在の状態PHP 7.2.0以降で非推奨。将来的に削除される可能性がある
アクセス方式の制約順次アクセスのみで、ランダムアクセスや逆順処理ができない
推奨される代替ZipArchive::numFilesとgetNameIndex()/locateName()によるインデックスベースのアクセス
注意点特定エントリ検索の非効率さ、総数把握のための全件走査、柔軟なアクセスパターンへの非対応

zip_read() は、非推奨の手続き型ZIP関数群における、エントリを1つずつ取り出すための中心的な関数でした。ZipArchive への移行によって、単純な全件走査だけでなく、特定エントリへの直接アクセス、総数の即座な把握、逆順処理やページネーションといった、より柔軟で効率的なアクセスパターンが可能になります。

タイトルとURLをコピーしました