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