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

PHP

はじめに

これまでの記事で、非推奨の手続き型ZIP関数群を数多く解説してきました。特に前回は、エントリを読み込み可能な状態にする zip_entry_open() を取り上げました。今回はその直後に呼び出される、一連の処理の中でも最も重要な役割を担う zip_entry_read() を解説します。この関数こそが、実際にエントリの中身(バイナリまたはテキストデータ)を取得する処理を行います。

これまでの一連の記事で繰り返し見てきた6段階の手順(開く→エントリ取得→エントリを開く→読む→閉じる→閉じる)の中で、zip_entry_read() はまさにデータそのものを手に入れる、最終目的にあたる関数です。前回までと同様、この関数もPHP 7.2.0以降で非推奨とされています。本記事では、その使い方と、ZipArchive を使った現代的で簡潔な代替実装を詳しく解説します。


関数概要

項目内容
関数名zip_entry_read()
所属拡張Zip拡張(手続き型インターフェース)
シグネチャzip_entry_read(resource $zip_entry, int $length = 1024): string|false
引数1$zip_entry — zip_entry_open() で開いたエントリのリソース
引数2$length — 読み込む最大バイト数(デフォルトは1024)
戻り値読み込んだデータの文字列。読み込み終端や失敗時は空文字列または false
対応バージョンPHP 4.1.0以降。PHP 7.2.0以降は非推奨
前提条件zip_entry_open() で事前にエントリを開いておく必要がある
推奨される代替ZipArchive::getFromName() / getFromIndex() / getStream()

デフォルト引数の落とし穴(イメージ図)

  zip_entry_read($entry)  ← 第2引数を省略した場合
              │
              ▼
  デフォルトの1024バイトだけが読み込まれる
              │
              ▼
  ┌───────────────────────────┐
  │ エントリの実際のサイズが1024バイトを   │
  │ 超えている場合、データの一部しか      │
  │ 取得できない(よくある誤用パターン)    │
  └───────────────────────────┘
              │
              ▼
  【正しい使い方】
  zip_entry_read($entry, zip_entry_filesize($entry))
  ← エントリ全体のサイズを明示的に指定する必要がある
   ★この記事の対象

ポイントは、zip_entry_read() の第2引数(読み込みバイト数)にデフォルト値が設定されているという点です。このデフォルト値(1024バイト)を意識せずに引数を省略してしまうと、大きなファイルの内容を一部しか取得できないという、非常によくある誤用パターンに陥ります。


実践サンプル7選

例1:正しい使い方(サイズを明示的に指定)

<?php

class CorrectLegacyReader
{
    /**
     * 注意: PHP 7.2.0以降、この一連の関数は非推奨です
     * 新規開発ではZipArchiveの使用を強く推奨します
     *
     * zip_entry_filesize()で取得した正確なサイズを渡すことで、
     * エントリ全体を漏れなく読み込む
     */
    public function readFullContent(string $zipPath, string $targetName): ?string
    {
        $zip = zip_open($zipPath);
        if (!is_resource($zip)) {
            return null;
        }

        $content = null;
        while ($entry = zip_read($zip)) {
            if (zip_entry_name($entry) === $targetName) {
                zip_entry_open($zip, $entry);
                // サイズを明示的に指定するのが正しい使い方
                $content = zip_entry_read($entry, zip_entry_filesize($entry));
                zip_entry_close($entry);
                break;
            }
        }
        zip_close($zip);

        return $content;
    }
}

$reader = new CorrectLegacyReader();
echo $reader->readFullContent('/tmp/sample.zip', 'readme.txt') . PHP_EOL;

例2:誤った使い方(デフォルト値による読み込み漏れの実演)

<?php

class IncorrectUsageDemonstrator
{
    /**
     * 第2引数を省略した場合、デフォルトの1024バイトしか
     * 読み込まれないという典型的な誤用を実演する(教育目的)
     */
    public function demonstrateTruncation(string $zipPath, string $targetName): array
    {
        $zip = zip_open($zipPath);
        $truncated = null;
        $fullSize = 0;

        if (is_resource($zip)) {
            while ($entry = zip_read($zip)) {
                if (zip_entry_name($entry) === $targetName) {
                    $fullSize = zip_entry_filesize($entry);
                    zip_entry_open($zip, $entry);
                    // 第2引数を省略 → デフォルトの1024バイトのみ読み込まれる
                    $truncated = zip_entry_read($entry);
                    zip_entry_close($entry);
                    break;
                }
            }
            zip_close($zip);
        }

        return [
            'full_size'      => $fullSize,
            'read_bytes'     => strlen((string) $truncated),
            'is_truncated'   => strlen((string) $truncated) < $fullSize,
        ];
    }
}

$demonstrator = new IncorrectUsageDemonstrator();
print_r($demonstrator->demonstrateTruncation('/tmp/sample.zip', 'large_file.txt'));

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

<?php

class ModernContentReader
{
    /**
     * ZipArchive::getFromName()は、
     * サイズの指定を意識する必要なく、常にエントリ全体を取得する
     */
    public function readFullContent(string $zipPath, string $targetName): ?string
    {
        $zip = new ZipArchive();
        if ($zip->open($zipPath) !== true) {
            return null;
        }

        $content = $zip->getFromName($targetName);
        $zip->close();

        return $content !== false ? $content : null;
    }
}

$reader = new ModernContentReader();
echo $reader->readFullContent('/tmp/sample.zip', 'readme.txt') . PHP_EOL;

例4:ループで繰り返し読み込むチャンク処理パターン(古い方式)

<?php

class ChunkedLegacyReader
{
    /**
     * 大きなファイルを一度に読み込まず、
     * ループで少しずつ読み込む古い方式のパターン
     */
    public function readInChunks(string $zipPath, string $targetName, int $chunkSize = 4096): ?string
    {
        $zip = zip_open($zipPath);
        if (!is_resource($zip)) {
            return null;
        }

        $result = '';
        while ($entry = zip_read($zip)) {
            if (zip_entry_name($entry) === $targetName) {
                zip_entry_open($zip, $entry);
                // 空文字列が返るまでループして読み進める
                while (($chunk = zip_entry_read($entry, $chunkSize)) !== '') {
                    $result .= $chunk;
                }
                zip_entry_close($entry);
                break;
            }
        }
        zip_close($zip);

        return $result;
    }
}

$reader = new ChunkedLegacyReader();
echo strlen($reader->readInChunks('/tmp/sample.zip', 'readme.txt')) . ' bytes' . PHP_EOL;

例5:ZipArchive::getStream()によるモダンなチャンク読み込み

<?php

class ModernChunkedReader
{
    /**
     * ZipArchive::getStream()を使うと、
     * 通常のファイルストリームと同様の感覚でチャンク読み込みができる
     */
    public function readInChunks(string $zipPath, string $entryName, int $chunkSize = 4096): string
    {
        $zip = new ZipArchive();
        if ($zip->open($zipPath) !== true) {
            return '';
        }

        $stream = $zip->getStream($entryName);
        if ($stream === false) {
            $zip->close();
            return '';
        }

        $result = '';
        while (!feof($stream)) {
            $result .= fread($stream, $chunkSize);
        }

        fclose($stream);
        $zip->close();

        return $result;
    }
}

$reader = new ModernChunkedReader();
echo strlen($reader->readInChunks('/tmp/sample.zip', 'readme.txt')) . ' bytes' . PHP_EOL;

例6:バイナリファイル(画像など)を正しく読み込むクラス

<?php

class BinaryFileExtractor
{
    /**
     * ZIPアーカイブ内の画像ファイルなどバイナリデータを
     * 正しく展開してファイルに保存する
     */
    public function extractBinaryFile(string $zipPath, string $entryName, string $outputPath): bool
    {
        $zip = new ZipArchive();
        if ($zip->open($zipPath) !== true) {
            return false;
        }

        $content = $zip->getFromName($entryName);
        $zip->close();

        if ($content === false) {
            return false;
        }

        return file_put_contents($outputPath, $content) !== false;
    }
}

$extractor = new BinaryFileExtractor();
var_dump($extractor->extractBinaryFile('/tmp/sample.zip', 'logo.png', '/tmp/extracted_logo.png'));

例7:読み込み内容のハッシュ値を検証する整合性チェッククラス

<?php

class ContentIntegrityChecker
{
    /**
     * 読み込んだ内容のサイズが期待通りであることを検証し、
     * 意図しない読み込み漏れ(デフォルト値による切り詰めなど)を検知する
     */
    public function verifyFullRead(string $zipPath, string $entryName): array
    {
        $zip = new ZipArchive();
        if ($zip->open($zipPath) !== true) {
            throw new RuntimeException('ZIPファイルを開けませんでした');
        }

        $stat = $zip->statName($entryName);
        $content = $zip->getFromName($entryName);
        $zip->close();

        $actualSize = strlen((string) $content);
        $expectedSize = $stat !== false ? $stat['size'] : 0;

        return [
            'expected_size' => $expectedSize,
            'actual_size'   => $actualSize,
            'is_complete'   => $actualSize === $expectedSize,
            'hash'          => md5((string) $content),
        ];
    }
}

$checker = new ContentIntegrityChecker();
print_r($checker->verifyFullRead('/tmp/sample.zip', 'readme.txt'));

関連関数との比較

関数/メソッド役割zip_entry_readとの違い
zip_entry_read()手続き型でオープン済みエントリの中身を読み込む本記事の対象。PHP 7.2.0以降は非推奨
zip_entry_open()手続き型でエントリを読み込み可能な状態にするzip_entry_read()の前提となる関数(前回記事を参照)
zip_entry_filesize()エントリの元サイズを取得zip_entry_read()に渡すサイズの算出に使われる
ZipArchive::getFromName()名前指定でエントリ内容を一括取得サイズ指定なしで、常にエントリ全体を取得できる
ZipArchive::getStream()エントリをストリームとして取得チャンク単位の読み込みが必要な場合の、より安全な代替

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

  1. 第2引数を省略すると意図せずデータが切り詰められる これがこの関数における最も典型的で危険な誤用パターンです。デフォルト値の1024バイトを超えるファイルに対して第2引数を省略すると、データの一部しか取得できません。必ず zip_entry_filesize() の結果を渡すか、ループで繰り返し読み込む必要があります(例1・例2を参照)。
  2. PHP 7.2.0以降でE_DEPRECATED警告が発生する これまでの記事と同様、この関数群を使用すると非推奨警告が発生します。早期の ZipArchive への移行が推奨されます。
  3. ZipArchive::getFromName()にはこの種の落とし穴が存在しない ZipArchive の対応するメソッドは、サイズを意識せず常にエントリ全体を取得する設計になっているため、zip_entry_read() で頻発していたこの種のバグは構造的に発生しません(例3を参照)。これも移行を推奨する重要な理由の一つです。
  4. zip_entry_open()を呼び出す前に使うとエラーになる zip_entry_read() は、事前に zip_entry_open() でエントリがオープンされていることを前提としています。この順序を守らないと正しく動作しません。
  5. バイナリデータの扱いに注意する 画像などのバイナリデータを読み込む場合も文字列として扱われますが、途中で意図しない文字コード変換処理などを挟まないよう注意しましょう。ZipArchive::getFromName() であれば、この点も含めてシンプルに扱えます(例6を参照)。

まとめ

観点まとめ
何をする関数かzip_entry_open()で開いたエントリの中身を実際に読み込む(手続き型インターフェース)
現在の状態PHP 7.2.0以降で非推奨。将来的に削除される可能性がある
最大の落とし穴第2引数(読み込みバイト数)のデフォルト値(1024)による意図しないデータの切り詰め
推奨される代替ZipArchive::getFromName() / getFromIndex() / getStream()
注意点事前のzip_entry_open()呼び出しが必須であること、正確なサイズ指定の重要性

zip_entry_read() は、非推奨の手続き型ZIP関数群の中でも、特に「デフォルト引数による意図しないデータ切り詰め」という典型的なバグを生みやすい関数でした。ZipArchive::getFromName() への移行は、非推奨警告の回避だけでなく、この構造的な誤用リスクそのものを解消してくれる、実務上非常に価値のある選択です。

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