[PHP]stream_get_meta_data完全ガイド|ストリームの内部情報を取得してタイムアウト・シーク可否・ラッパー種別を正確に把握する

PHP

はじめに

PHPでストリームを扱う際、「このストリームはシーク可能か?」「タイムアウトが発生していないか?」「ブロッキングモードか否か?」といった内部状態を確認したい場面があります。

stream_get_meta_data() は、ストリームリソースに関するメタ情報を連想配列で返す関数です。ファイル・ネットワーク・メモリストリームを問わず、ラッパー種別・読み取りバッファの状態・タイムアウト発生フラグなど、ストリームの「素性」を一括取得できます。


関数の基本情報

項目内容
関数名stream_get_meta_data()
対応バージョンPHP 4.3.0 以降
返り値array(メタ情報の連想配列)
カテゴリストリーム関数

構文

stream_get_meta_data(resource $stream): array

パラメータ

パラメータ説明
$streamresource情報を取得するストリームリソース

返り値の構造

[
    'timed_out'    => bool,   // タイムアウトが発生したか
    'blocked'      => bool,   // ブロッキングI/Oモードか
    'eof'          => bool,   // EOF(ストリーム終端)に達したか
    'unread_bytes' => int,    // 内部バッファに残っている未読バイト数
    'stream_type'  => string, // ストリームの実装種別(例: "STDIO", "tcp_socket/ssl")
    'wrapper_type' => string, // ラッパー種別(例: "plainfile", "http", "PHP")
    'wrapper_data' => mixed,  // ラッパー固有のデータ(HTTP なら応答ヘッダー配列など)
    'filters'      => array,  // アタッチされているフィルター名の配列
    'mode'         => string, // fopen() に渡したモード文字列(例: "r", "w+b")
    'seekable'     => bool,   // シーク可能か
    'uri'          => string, // ストリームに関連付けられたURI(ファイルパス・URL)
]

各キーの詳細

キー説明
timed_outbool直前のデータ読み取りでタイムアウトが発生した場合 truestream_set_timeout() と組み合わせて使う
blockedboolブロッキングモードなら true、ノンブロッキングなら false
eofboolEOF に達していれば truefeof() の代替として使える
unread_bytesintPHP 内部の読み取りバッファに残っているバイト数(正確な値でない場合あり)
stream_typestringストリームの実装種別(STDIOtcp_sockettcp_socket/ssl など)
wrapper_typestringラッパー種別(plainfilehttpftpPHP など)
wrapper_datamixedラッパー依存。HTTP ラッパーなら受信ヘッダーの配列。ファイルなら null
filtersarrayアタッチ済みフィルター名の配列(フィルターなしなら空配列)
modestringストリームオープン時のモード(rwr+b など)
seekableboolfseek() が使えるなら true(ネットワークストリームは通常 false
uristringストリームに紐付いたURI(ファイルパスやURL)

基本的な使い方

<?php

// ファイルストリームのメタ情報
$fp   = fopen('/tmp/sample.txt', 'r+');
$meta = stream_get_meta_data($fp);

echo "URI        : " . $meta['uri']          . PHP_EOL;
echo "モード     : " . $meta['mode']         . PHP_EOL;
echo "シーク可能 : " . ($meta['seekable'] ? 'YES' : 'NO') . PHP_EOL;
echo "EOF        : " . ($meta['eof']       ? 'YES' : 'NO') . PHP_EOL;
echo "ラッパー   : " . $meta['wrapper_type'] . PHP_EOL;
echo "ストリーム : " . $meta['stream_type']  . PHP_EOL;

fclose($fp);

// php://memory のメタ情報
$mem  = fopen('php://memory', 'w+');
$meta = stream_get_meta_data($mem);
var_dump($meta['seekable']);   // bool(true)
var_dump($meta['wrapper_type']); // string(3) "PHP"
fclose($mem);

実践的なクラスベースの活用例


例1:ストリーム情報インスペクタークラス(StreamInspector)

任意のストリームのメタ情報を構造化して取得し、人間が読みやすい形式でレポートするインスペクタークラスです。

<?php

class StreamInspector
{
    private array $meta;

    public function __construct(private $stream)
    {
        $this->meta = stream_get_meta_data($stream);
    }

    public function isSeekable(): bool  { return $this->meta['seekable']; }
    public function isBlocked(): bool   { return $this->meta['blocked']; }
    public function isEof(): bool       { return $this->meta['eof']; }
    public function isTimedOut(): bool  { return $this->meta['timed_out']; }

    public function getMode(): string        { return $this->meta['mode']; }
    public function getUri(): string         { return $this->meta['uri'] ?? ''; }
    public function getWrapperType(): string { return $this->meta['wrapper_type']; }
    public function getStreamType(): string  { return $this->meta['stream_type']; }
    public function getUnreadBytes(): int    { return $this->meta['unread_bytes']; }

    /** アタッチ済みフィルターの一覧 */
    public function getFilters(): array { return $this->meta['filters'] ?? []; }

    /** 書き込み可能モードか判定 */
    public function isWritable(): bool
    {
        return str_contains($this->meta['mode'], 'w')
            || str_contains($this->meta['mode'], 'a')
            || str_contains($this->meta['mode'], '+');
    }

    /** 読み取り可能モードか判定 */
    public function isReadable(): bool
    {
        return !str_starts_with($this->meta['mode'], 'w')
            || str_contains($this->meta['mode'], '+');
    }

    /** メタ情報を再取得する(タイムアウト確認など動的チェック用) */
    public function refresh(): void
    {
        $this->meta = stream_get_meta_data($this->stream);
    }

    public function report(): void
    {
        echo "=== Stream Inspector ===" . PHP_EOL;
        echo sprintf("  URI          : %s\n",  $this->getUri());
        echo sprintf("  モード       : %s\n",  $this->getMode());
        echo sprintf("  ラッパー     : %s\n",  $this->getWrapperType());
        echo sprintf("  ストリーム型 : %s\n",  $this->getStreamType());
        echo sprintf("  シーク可能   : %s\n",  $this->isSeekable() ? 'YES' : 'NO');
        echo sprintf("  書き込み可能 : %s\n",  $this->isWritable() ? 'YES' : 'NO');
        echo sprintf("  読み取り可能 : %s\n",  $this->isReadable() ? 'YES' : 'NO');
        echo sprintf("  ブロッキング : %s\n",  $this->isBlocked()  ? 'YES' : 'NO');
        echo sprintf("  EOF          : %s\n",  $this->isEof()      ? 'YES' : 'NO');
        echo sprintf("  未読バイト数 : %d\n",  $this->getUnreadBytes());
        if (!empty($this->getFilters())) {
            echo sprintf("  フィルター   : %s\n", implode(', ', $this->getFilters()));
        }
    }
}

// 使用例
$fp        = fopen('php://memory', 'w+');
$inspector = new StreamInspector($fp);

stream_filter_append($fp, 'string.toupper', STREAM_FILTER_WRITE);
$inspector->refresh();
$inspector->report();

fclose($fp);

// 出力例:
// === Stream Inspector ===
//   URI          : php://memory
//   モード       : w+
//   ラッパー     : PHP
//   ストリーム型 : PHP
//   シーク可能   : YES
//   書き込み可能 : YES
//   読み取り可能 : YES
//   ブロッキング : YES
//   EOF          : NO
//   未読バイト数 : 0
//   フィルター   : string.toupper

例2:ネットワークストリームタイムアウト監視クラス(TimeoutAwareReader)

stream_set_timeout() でタイムアウトを設定し、読み取りのたびに stream_get_meta_data()timed_out フラグを確認して、タイムアウト発生時にリトライや例外をハンドリングするクラスです。

<?php

class StreamTimeoutException extends \RuntimeException {}

class TimeoutAwareReader
{
    private int   $timeoutSec;
    private int   $timeoutUsec;
    private int   $maxRetries;

    public function __construct(
        private $stream,
        int $timeoutSec  = 5,
        int $timeoutUsec = 0,
        int $maxRetries  = 3
    ) {
        $this->timeoutSec  = $timeoutSec;
        $this->timeoutUsec = $timeoutUsec;
        $this->maxRetries  = $maxRetries;

        stream_set_timeout($this->stream, $timeoutSec, $timeoutUsec);
    }

    /**
     * 指定デリミタまでデータを読み取る(タイムアウトをリトライ付きで処理)
     *
     * @throws StreamTimeoutException リトライ上限を超えた場合
     */
    public function readLine(int $maxLength = 4096, string $delimiter = "\n"): string|false
    {
        $retries = 0;

        while ($retries <= $this->maxRetries) {
            $data = stream_get_line($this->stream, $maxLength, $delimiter);
            $meta = stream_get_meta_data($this->stream);

            if ($meta['timed_out']) {
                $retries++;
                $elapsed = $this->timeoutSec + $this->timeoutUsec / 1_000_000;
                error_log("タイムアウト発生({$elapsed}秒)。リトライ {$retries}/{$this->maxRetries}");

                if ($retries > $this->maxRetries) {
                    throw new StreamTimeoutException(
                        "ストリーム読み取りが {$this->maxRetries} 回タイムアウトしました"
                    );
                }
                continue;
            }

            if ($meta['eof']) {
                return false;
            }

            return $data;
        }

        return false;
    }

    /**
     * 全行をジェネレータで返す(タイムアウトを自動リトライ)
     *
     * @return \Generator<int, string>
     */
    public function lines(): \Generator
    {
        while (true) {
            try {
                $line = $this->readLine();
                if ($line === false) break;
                yield $line;
            } catch (StreamTimeoutException $e) {
                error_log("致命的タイムアウト: " . $e->getMessage());
                break;
            }
        }
    }

    /**
     * タイムアウト設定を変更する
     */
    public function setTimeout(int $sec, int $usec = 0): void
    {
        $this->timeoutSec  = $sec;
        $this->timeoutUsec = $usec;
        stream_set_timeout($this->stream, $sec, $usec);
    }
}

// 使用例(ローカルファイルでの動作確認)
$fp = fopen('php://memory', 'r+');
fwrite($fp, "line1\nline2\nline3\n");
rewind($fp);

$reader = new TimeoutAwareReader($fp, timeoutSec: 5);

foreach ($reader->lines() as $i => $line) {
    echo "行{$i}: {$line}" . PHP_EOL;
}
fclose($fp);

// 出力:
// 行0: line1
// 行1: line2
// 行2: line3

例3:HTTPラッパーレスポンスパーサー(HttpWrapperMetaReader)

file_get_contents()fopen() で HTTP ストリームを開いた後、stream_get_meta_data()wrapper_data に格納されるレスポンスヘッダーを解析するクラスです。

<?php

class HttpWrapperMetaReader
{
    private array $meta;
    private array $headers = [];
    private int   $statusCode = 0;
    private string $statusMessage = '';

    public function __construct(private $stream)
    {
        $this->meta = stream_get_meta_data($stream);
        $this->parseHeaders();
    }

    private function parseHeaders(): void
    {
        // wrapper_data には HTTP ヘッダー行の配列が入る
        $wrapperData = $this->meta['wrapper_data'] ?? [];
        if (!is_array($wrapperData)) return;

        foreach ($wrapperData as $line) {
            // ステータス行
            if (preg_match('/^HTTP\/[\d.]+\s+(\d+)\s+(.*)$/', $line, $m)) {
                $this->statusCode    = (int)$m[1];
                $this->statusMessage = trim($m[2]);
                continue;
            }
            // ヘッダー行
            if (str_contains($line, ':')) {
                [$name, $value] = explode(':', $line, 2);
                $this->headers[strtolower(trim($name))][] = trim($value);
            }
        }
    }

    public function getStatusCode(): int       { return $this->statusCode; }
    public function getStatusMessage(): string { return $this->statusMessage; }
    public function isSuccess(): bool          { return $this->statusCode >= 200 && $this->statusCode < 300; }
    public function isRedirect(): bool         { return $this->statusCode >= 300 && $this->statusCode < 400; }

    /** 指定ヘッダーの最初の値を返す */
    public function getHeader(string $name): ?string
    {
        return $this->headers[strtolower($name)][0] ?? null;
    }

    /** 指定ヘッダーの全値を返す */
    public function getHeaders(string $name): array
    {
        return $this->headers[strtolower($name)] ?? [];
    }

    /** Content-Type から charset を抽出する */
    public function getCharset(): ?string
    {
        $ct = $this->getHeader('content-type') ?? '';
        if (preg_match('/charset=([^\s;]+)/i', $ct, $m)) {
            return strtolower($m[1]);
        }
        return null;
    }

    public function isSeekable(): bool    { return $this->meta['seekable']; }
    public function isTimedOut(): bool    { return $this->meta['timed_out']; }
    public function getWrapperData(): array { return $this->meta['wrapper_data'] ?? []; }

    public function dump(): void
    {
        echo "ステータス  : {$this->statusCode} {$this->statusMessage}" . PHP_EOL;
        echo "Content-Type: " . ($this->getHeader('content-type') ?? '不明') . PHP_EOL;
        echo "Charset     : " . ($this->getCharset() ?? '不明') . PHP_EOL;
        echo "シーク可能  : " . ($this->isSeekable() ? 'YES' : 'NO') . PHP_EOL;
    }
}

// 使用例(モックストリームで動作確認)
$mockMeta = [
    'timed_out'    => false,
    'blocked'      => true,
    'eof'          => false,
    'unread_bytes' => 0,
    'stream_type'  => 'tcp_socket/ssl',
    'wrapper_type' => 'http',
    'wrapper_data' => [
        'HTTP/1.1 200 OK',
        'Content-Type: text/html; charset=UTF-8',
        'Content-Length: 1024',
        'Cache-Control: no-cache',
    ],
    'filters'      => [],
    'mode'         => 'r',
    'seekable'     => false,
    'uri'          => 'https://example.com/',
];

// 実際の HTTP ストリームを使う場合:
// $ctx = stream_context_create(['http' => ['timeout' => 10]]);
// $fp  = fopen('https://example.com/', 'rb', false, $ctx);
// $reader = new HttpWrapperMetaReader($fp);

// モックで確認
$fp = fopen('php://memory', 'r+');
// ※ 実際の HTTP ストリームでは wrapper_data が自動的に設定される
$reader = new HttpWrapperMetaReader($fp);
echo "wrapper_type: " . stream_get_meta_data($fp)['wrapper_type'] . PHP_EOL; // PHP
fclose($fp);

例4:シーク可否対応ユニバーサルリーダー(UniversalStreamReader)

stream_get_meta_data() でシーク可否を確認し、可能なら fseek()、不可能ならバッファリングで対応するユニバーサルリーダーです。

<?php

class UniversalStreamReader
{
    private bool   $seekable;
    private string $buffer = '';
    private bool   $bufferLoaded = false;

    public function __construct(private $stream)
    {
        $meta           = stream_get_meta_data($stream);
        $this->seekable = $meta['seekable'];
    }

    /**
     * ストリームの全内容を返す(シーク可否を自動判定)
     */
    public function readAll(): string
    {
        if ($this->seekable) {
            return stream_get_contents($this->stream, offset: 0);
        }

        // シーク不可:一度読んだらバッファに保持
        if (!$this->bufferLoaded) {
            $this->buffer       = stream_get_contents($this->stream);
            $this->bufferLoaded = true;
        }
        return $this->buffer;
    }

    /**
     * オフセットを指定して読む(シーク不可ストリームはバッファ経由)
     */
    public function readFrom(int $offset, int $length = -1): string
    {
        if ($this->seekable) {
            return stream_get_contents($this->stream, length: $length, offset: $offset);
        }

        $all = $this->readAll();
        return $length === -1
            ? substr($all, $offset)
            : substr($all, $offset, $length);
    }

    /**
     * 内容を行配列で返す
     *
     * @return string[]
     */
    public function lines(): array
    {
        return explode("\n", rtrim($this->readAll(), "\n"));
    }

    /**
     * キーワードを含む行を返す
     *
     * @return string[]
     */
    public function grep(string $keyword): array
    {
        return array_values(array_filter(
            $this->lines(),
            fn($line) => str_contains($line, $keyword)
        ));
    }

    public function isSeekable(): bool { return $this->seekable; }
}

// 使用例
$fp     = fopen('php://memory', 'r+');
fwrite($fp, "apple\nbanana\napricot\ncherry\navocado\n");
rewind($fp);

$reader = new UniversalStreamReader($fp);
echo "シーク可能: " . ($reader->isSeekable() ? 'YES' : 'NO') . PHP_EOL;

echo "--- 'a' を含む行 ---" . PHP_EOL;
foreach ($reader->grep('a') as $line) {
    echo "  {$line}" . PHP_EOL;
}

echo "--- offset=7 から12バイト ---" . PHP_EOL;
echo $reader->readFrom(7, 12) . PHP_EOL; // "banana\napri" の一部

fclose($fp);

// 出力:
// シーク可能: YES
// --- 'a' を含む行 ---
//   apple
//   banana
//   apricot
//   avocado
// --- offset=7 から12バイト ---
// banana
// apri

例5:ストリームモード検証クラス(StreamModeGuard)

stream_get_meta_data()mode キーを解析し、ストリームへの操作(読み取り・書き込み・追記・シーク)が許可されているかを事前に検証するガードクラスです。

<?php

class StreamModeException extends \LogicException {}

class StreamModeGuard
{
    private string $mode;
    private bool   $seekable;

    public function __construct(private $stream)
    {
        $meta           = stream_get_meta_data($stream);
        $this->mode     = $meta['mode'];
        $this->seekable = $meta['seekable'];
    }

    public function canRead(): bool
    {
        // 'w', 'a', 'x', 'c' のみ(+なし)は読み取り不可
        return str_contains($this->mode, 'r')
            || str_contains($this->mode, '+');
    }

    public function canWrite(): bool
    {
        return str_contains($this->mode, 'w')
            || str_contains($this->mode, 'a')
            || str_contains($this->mode, 'x')
            || str_contains($this->mode, 'c')
            || str_contains($this->mode, '+');
    }

    public function canSeek(): bool  { return $this->seekable; }
    public function isAppend(): bool { return str_contains($this->mode, 'a'); }
    public function isBinary(): bool { return str_contains($this->mode, 'b'); }

    /**
     * 読み取り操作の前に呼び出してガードする
     *
     * @throws StreamModeException 読み取り不可の場合
     */
    public function assertReadable(): void
    {
        if (!$this->canRead()) {
            throw new StreamModeException(
                "ストリーム(モード: {$this->mode})は読み取り不可です"
            );
        }
    }

    /**
     * 書き込み操作の前に呼び出してガードする
     *
     * @throws StreamModeException 書き込み不可の場合
     */
    public function assertWritable(): void
    {
        if (!$this->canWrite()) {
            throw new StreamModeException(
                "ストリーム(モード: {$this->mode})は書き込み不可です"
            );
        }
    }

    /**
     * シーク操作の前に呼び出してガードする
     *
     * @throws StreamModeException シーク不可の場合
     */
    public function assertSeekable(): void
    {
        if (!$this->canSeek()) {
            throw new StreamModeException(
                "ストリームはシーク不可です(stream_type: " .
                stream_get_meta_data($this->stream)['stream_type'] . ")"
            );
        }
    }

    public function summary(): string
    {
        return sprintf(
            'mode=%s read=%s write=%s seek=%s append=%s binary=%s',
            $this->mode,
            $this->canRead()  ? 'YES' : 'NO',
            $this->canWrite() ? 'YES' : 'NO',
            $this->canSeek()  ? 'YES' : 'NO',
            $this->isAppend() ? 'YES' : 'NO',
            $this->isBinary() ? 'YES' : 'NO',
        );
    }
}

// 使用例
foreach (['r', 'w', 'r+', 'a', 'w+b'] as $mode) {
    $fp    = fopen('php://memory', $mode);
    $guard = new StreamModeGuard($fp);
    echo $guard->summary() . PHP_EOL;
    fclose($fp);
}

// 書き込み専用ストリームへの読み取りをガードする例
$fp    = fopen('php://memory', 'w');
$guard = new StreamModeGuard($fp);

try {
    $guard->assertReadable();
} catch (StreamModeException $e) {
    echo "ガード発動: " . $e->getMessage() . PHP_EOL;
}
fclose($fp);

// 出力:
// mode=r  read=YES write=NO  seek=YES append=NO  binary=NO
// mode=w  read=NO  write=YES seek=YES append=NO  binary=NO
// mode=r+ read=YES write=YES seek=YES append=NO  binary=NO
// mode=a  read=NO  write=YES seek=YES append=YES binary=NO
// mode=w+b read=YES write=YES seek=YES append=NO binary=YES
// ガード発動: ストリーム(モード: w)は読み取り不可です

例6:フィルター付きストリーム診断クラス(FilteredStreamDiagnostics)

アタッチされているフィルターの一覧を stream_get_meta_data()filters キーから取得し、フィルターの適用順や数を検証する診断クラスです。

<?php

class FilteredStreamDiagnostics
{
    /**
     * ストリームに現在アタッチされているフィルターを返す
     *
     * @return string[]
     */
    public function getAttachedFilters($stream): array
    {
        return stream_get_meta_data($stream)['filters'] ?? [];
    }

    /**
     * 期待するフィルターが全てアタッチされているか検証する
     *
     * @param  string[] $expected
     * @return array{ok: bool, missing: string[], extra: string[]}
     */
    public function verify($stream, array $expected): array
    {
        $attached = $this->getAttachedFilters($stream);
        return [
            'ok'      => empty(array_diff($expected, $attached)),
            'missing' => array_values(array_diff($expected, $attached)),
            'extra'   => array_values(array_diff($attached, $expected)),
        ];
    }

    /**
     * フィルターの適用順を確認するレポートを出力する
     */
    public function report($stream): void
    {
        $meta    = stream_get_meta_data($stream);
        $filters = $meta['filters'] ?? [];

        echo "=== フィルター診断 ===" . PHP_EOL;
        echo "URI         : " . ($meta['uri'] ?? 'N/A') . PHP_EOL;
        echo "アタッチ数  : " . count($filters) . PHP_EOL;

        if (empty($filters)) {
            echo "(フィルターなし)" . PHP_EOL;
            return;
        }

        echo "適用順:" . PHP_EOL;
        foreach ($filters as $i => $filter) {
            echo sprintf("  [%d] %s\n", $i + 1, $filter);
        }
    }
}

// 使用例
$fp   = fopen('php://memory', 'w+');
$diag = new FilteredStreamDiagnostics();

stream_filter_append($fp, 'string.toupper',  STREAM_FILTER_WRITE);
stream_filter_append($fp, 'string.rot13',    STREAM_FILTER_WRITE);

$diag->report($fp);

// 期待するフィルターの検証
$result = $diag->verify($fp, ['string.toupper', 'string.rot13']);
echo "検証OK: " . ($result['ok'] ? 'YES' : 'NO') . PHP_EOL;

$result2 = $diag->verify($fp, ['string.toupper', 'string.tolower']);
echo "検証OK: " . ($result2['ok'] ? 'YES' : 'NO') . PHP_EOL;
echo "不足  : " . implode(', ', $result2['missing']) . PHP_EOL;
echo "余分  : " . implode(', ', $result2['extra'])   . PHP_EOL;

fclose($fp);

// 出力:
// === フィルター診断 ===
// URI         : php://memory
// アタッチ数  : 2
// 適用順:
//   [1] string.toupper
//   [2] string.rot13
// 検証OK: YES
// 検証OK: NO
// 不足  : string.tolower
// 余分  : string.rot13

例7:ストリームヘルスチェッカー(StreamHealthChecker)

stream_get_meta_data() を定期的に呼び出してストリームの健全性(EOF・タイムアウト・ブロッキング状態)を監視し、異常を検知したら通知するヘルスチェッカークラスです。

<?php

class StreamHealthStatus
{
    public function __construct(
        public readonly bool   $healthy,
        public readonly bool   $eof,
        public readonly bool   $timedOut,
        public readonly bool   $blocked,
        public readonly int    $unreadBytes,
        public readonly string $checkedAt,
        public readonly array  $issues,
    ) {}

    public function __toString(): string
    {
        $status = $this->healthy ? '✓ HEALTHY' : '✗ UNHEALTHY';
        $parts  = ["{$status} @ {$this->checkedAt}"];
        foreach ($this->issues as $issue) {
            $parts[] = "  ⚠ {$issue}";
        }
        return implode(PHP_EOL, $parts);
    }
}

class StreamHealthChecker
{
    /** @var callable[] */
    private array $onUnhealthy = [];

    public function __construct(private $stream) {}

    /**
     * ヘルスチェックを実行して結果を返す
     */
    public function check(): StreamHealthStatus
    {
        $meta   = stream_get_meta_data($this->stream);
        $issues = [];

        if ($meta['eof']) {
            $issues[] = 'EOF に達しています';
        }
        if ($meta['timed_out']) {
            $issues[] = 'タイムアウトが発生しています';
        }
        if (!$meta['blocked']) {
            $issues[] = 'ノンブロッキングモードです(意図的でなければ要確認)';
        }
        if ($meta['unread_bytes'] > 65536) {
            $issues[] = "未読バッファが大きすぎます({$meta['unread_bytes']} バイト)";
        }

        $healthy = empty($issues);
        $status  = new StreamHealthStatus(
            healthy:     $healthy,
            eof:         $meta['eof'],
            timedOut:    $meta['timed_out'],
            blocked:     $meta['blocked'],
            unreadBytes: $meta['unread_bytes'],
            checkedAt:   date('Y-m-d H:i:s'),
            issues:      $issues,
        );

        if (!$healthy) {
            foreach ($this->onUnhealthy as $callback) {
                $callback($status);
            }
        }

        return $status;
    }

    /**
     * 異常検知時のコールバックを登録する
     */
    public function onUnhealthy(callable $callback): static
    {
        $this->onUnhealthy[] = $callback;
        return $this;
    }

    /**
     * $intervalMs ミリ秒ごとに $times 回チェックし、全結果を返す
     *
     * @return StreamHealthStatus[]
     */
    public function monitor(int $times = 5, int $intervalMs = 1000): array
    {
        $results = [];
        for ($i = 0; $i < $times; $i++) {
            $results[] = $this->check();
            if ($i < $times - 1) {
                usleep($intervalMs * 1000);
            }
        }
        return $results;
    }
}

// 使用例
$fp      = fopen('php://memory', 'r+');
fwrite($fp, "test data\n");
rewind($fp);

$checker = new StreamHealthChecker($fp);
$checker->onUnhealthy(function (StreamHealthStatus $s) {
    error_log("ストリーム異常: " . implode(', ', $s->issues));
});

// 通常状態
echo $checker->check() . PHP_EOL;

// EOF 状態を意図的に作る
stream_get_contents($fp); // 全て読み切る
echo $checker->check() . PHP_EOL;

fclose($fp);

// 出力:
// ✓ HEALTHY @ 2025-05-13 10:00:00
// ✗ UNHEALTHY @ 2025-05-13 10:00:00
//   ⚠ EOF に達しています

関連する関数との比較

関数役割
stream_get_meta_data()ストリームのメタ情報(モード・シーク可否・タイムアウト等)を取得
stream_set_timeout()ストリームのタイムアウトを設定(timed_out フラグと連携)
stream_set_blocking()ストリームのブロッキングモードを切り替え(blocked フラグと連携)
fstat()ファイルストリームのファイルシステム情報(サイズ・更新日時等)を取得
get_resource_type()リソースの型名streamcurl など)を返す
stream_get_filters()利用可能なフィルター名一覧を返す(filters キーとは別)

注意点とベストプラクティス

1. timed_out は「発生したか」であり「現在中か」ではない

timed_out は直前の読み取り操作でタイムアウトが起きたことを示します。次の読み取りが成功すれば false に戻ります。定期監視する場合は毎回 stream_get_meta_data() を再呼び出しして確認しましょう。

2. wrapper_data の形式はラッパーによって異なる

HTTP ラッパーではレスポンスヘッダーの配列ですが、他のラッパーでは null や別の形式になります。is_array() で確認してから参照しましょう。

$wrapperData = stream_get_meta_data($fp)['wrapper_data'];
if (is_array($wrapperData)) {
    // HTTP ヘッダーを処理
}

3. filters キーはアタッチ済みのフィルター名のみ

stream_get_filters()利用可能な全フィルター一覧を返すのに対し、stream_get_meta_data()filters キーはそのストリームに現在アタッチされているフィルターの配列です。

4. unread_bytes は目安値

内部バッファの未読バイト数は実装依存の概算値であり、正確なバイト数とは限りません。厳密なバイト数管理には ftell() や自前のカウンタを使いましょう。


まとめ

キー主な用途
seekablefseek() 前の可否確認
timed_outネットワーク読み取りのタイムアウト検知
eofストリーム終端の確認
mode読み書き可否の判定
wrapper_typeHTTP / ファイル / メモリの種別判定
wrapper_dataHTTP レスポンスヘッダーの取得
filtersアタッチ済みフィルターの確認
stream_typeストリーム実装種別の把握
uriストリームに紐付いたパス・URL の確認

stream_get_meta_data() はストリームの「現在の状態」を知るための万能な診断関数です。タイムアウト監視・シーク可否の分岐・フィルター確認・HTTPヘッダー取得など、ストリームを堅牢に扱うあらゆる場面で活用できます。

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