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