[PHP]stream_is_local完全ガイド|ストリームがローカルリソースかどうかを判定してセキュアなファイル処理を実現する

PHP

はじめに

PHPでストリームを扱う際、「このストリームはローカルファイルか、それともネットワーク越しのリソースか」を判定したい場面があります。セキュリティチェック・キャッシュ戦略の分岐・パフォーマンス最適化など、ローカル/リモートの区別が重要なケースは少なくありません。

stream_is_local() は、ストリームまたはURLがローカルリソースを指しているかどうかを bool で返す関数です。ファイルパス・ストリームリソース・URL文字列のいずれも受け付ける柔軟な判定関数で、ストリームの種別に応じた処理の分岐に活用できます。


関数の基本情報

項目内容
関数名stream_is_local()
対応バージョンPHP 5.2.4 以降
返り値bool
カテゴリストリーム関数

構文

stream_is_local(resource|string $stream): bool

パラメータ

パラメータ説明
$streamresource|string判定するストリームリソース、またはURL・ファイルパスの文字列

返り値

  • true:ローカルリソース(ローカルファイルシステム上のストリーム)
  • false:リモートリソース(ネットワーク越しのストリームなど)

基本的な使い方

<?php

// ストリームリソースで判定
$local = fopen('/tmp/test.txt', 'w+');
var_dump(stream_is_local($local)); // bool(true)
fclose($local);

$memory = fopen('php://memory', 'r+');
var_dump(stream_is_local($memory)); // bool(true)

// 文字列(URLやパス)で判定
var_dump(stream_is_local('/var/log/app.log'));     // bool(true)
var_dump(stream_is_local('file:///tmp/test.txt')); // bool(true)
var_dump(stream_is_local('http://example.com/'));  // bool(false)
var_dump(stream_is_local('ftp://files.example.com/data.csv')); // bool(false)
var_dump(stream_is_local('compress.zlib:///tmp/file.gz'));     // bool(true)

ローカル/リモート判定の基準

stream_is_local() は、ストリームラッパーの is_url フラグに基づいて判定します。

ストリーム / URLstream_is_local()理由
/path/to/filetrueローカルファイルシステム
file:///path/to/filetruefile:// ラッパーはローカル
php://memorytruephp:// ラッパーはローカル
php://temptruephp:// ラッパーはローカル
php://stdintruephp:// ラッパーはローカル
compress.zlib:///tmp/a.gztrueローカルファイルの圧縮ラッパー
http://example.com/falseネットワークラッパー
https://example.com/falseネットワークラッパー
ftp://example.com/falseネットワークラッパー
ftps://example.com/falseネットワークラッパー
カスタムラッパーラッパー定義次第$wrapper_data['is_url'] に依存

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


例1:ストリーム種別ルーター(StreamRouter)

ストリームがローカルかリモートかに応じて処理を振り分けるルータークラスです。ローカルは高速直接読み取り、リモートはタイムアウト付きの慎重な読み取りを行います。

<?php

class StreamRouter
{
    private int $remoteTimeout;
    private int $localChunkSize;
    private int $remoteChunkSize;

    public function __construct(
        int $remoteTimeout   = 30,
        int $localChunkSize  = 65536,  // 64KB
        int $remoteChunkSize = 8192,   // 8KB
    ) {
        $this->remoteTimeout   = $remoteTimeout;
        $this->localChunkSize  = $localChunkSize;
        $this->remoteChunkSize = $remoteChunkSize;
    }

    /**
     * ストリームまたはパス文字列から内容を読み取る
     * ローカル/リモートを自動判定して最適な方法で処理する
     */
    public function read(mixed $source): string
    {
        if (is_string($source)) {
            return $this->readFromPath($source);
        }

        if (stream_is_local($source)) {
            return $this->readLocal($source);
        }

        return $this->readRemote($source);
    }

    private function readFromPath(string $path): string
    {
        if (stream_is_local($path)) {
            // ローカルパスは file_get_contents で高速読み取り
            $content = file_get_contents($path);
            return $content !== false ? $content : '';
        }

        // リモートURLはコンテキスト付きで慎重に読み取る
        $context = stream_context_create([
            'http' => [
                'timeout'     => $this->remoteTimeout,
                'user_agent'  => 'PHP StreamRouter/1.0',
                'follow_location' => 1,
                'max_redirects'   => 3,
            ],
            'ssl' => [
                'verify_peer'      => true,
                'verify_peer_name' => true,
            ],
        ]);
        $content = file_get_contents($path, false, $context);
        return $content !== false ? $content : '';
    }

    private function readLocal($stream): string
    {
        // ローカルは大きなチャンクで高速読み取り
        return stream_get_contents($stream, offset: 0) ?: '';
    }

    private function readRemote($stream): string
    {
        // リモートはタイムアウト設定して小さいチャンクで読み取り
        stream_set_timeout($stream, $this->remoteTimeout);
        $content = '';
        while (!feof($stream)) {
            $chunk = fread($stream, $this->remoteChunkSize);
            if ($chunk === false) break;
            $content .= $chunk;

            $meta = stream_get_meta_data($stream);
            if ($meta['timed_out']) {
                throw new \RuntimeException('リモートストリームの読み取りがタイムアウトしました');
            }
        }
        return $content;
    }

    /**
     * ローカル/リモートの判定結果と推奨設定を返す
     */
    public function inspect(mixed $source): array
    {
        $isLocal = is_string($source)
            ? stream_is_local($source)
            : stream_is_local($source);

        return [
            'is_local'   => $isLocal,
            'type'       => $isLocal ? 'local' : 'remote',
            'chunk_size' => $isLocal ? $this->localChunkSize : $this->remoteChunkSize,
            'timeout'    => $isLocal ? null : $this->remoteTimeout,
        ];
    }
}

// 使用例
$router = new StreamRouter();

// ローカルストリーム
$fp = fopen('php://memory', 'w+');
fwrite($fp, "ローカルデータ");
$info = $router->inspect($fp);
echo "種別: {$info['type']}, チャンクサイズ: {$info['chunk_size']}" . PHP_EOL;
echo $router->read($fp) . PHP_EOL;
fclose($fp);

// パス文字列の判定
foreach (['/tmp/test.txt', 'http://example.com/', 'php://memory'] as $path) {
    $info = $router->inspect($path);
    echo "{$path} → {$info['type']}" . PHP_EOL;
}

// 出力:
// 種別: local, チャンクサイズ: 65536
// ローカルデータ
// /tmp/test.txt → local
// http://example.com/ → remote
// php://memory → local

例2:セキュアファイルバリデーター(SecureStreamValidator)

アップロードファイルや外部から受け取ったパスに対して、stream_is_local() でローカルファイルであることを確認し、パストラバーサル攻撃やSSRF(Server-Side Request Forgery)を防ぐバリデーターです。

<?php

class StreamSecurityException extends \RuntimeException {}

class SecureStreamValidator
{
    /** @var string[] 許可するベースディレクトリ */
    private array $allowedBasePaths;

    /** @var string[] 拒否するファイル拡張子 */
    private array $deniedExtensions;

    public function __construct(
        array $allowedBasePaths  = [],
        array $deniedExtensions  = ['php', 'phtml', 'phar', 'exe', 'sh', 'bat']
    ) {
        $this->allowedBasePaths = array_map('realpath', array_filter($allowedBasePaths, 'is_dir'));
        $this->deniedExtensions = $deniedExtensions;
    }

    /**
     * パスまたはストリームを総合的にバリデーションする
     *
     * @throws StreamSecurityException セキュリティ要件を満たさない場合
     */
    public function validate(string|resource $source): void
    {
        // 1. ローカルリソースであることを確認(SSRF 防止)
        if (!stream_is_local($source)) {
            throw new StreamSecurityException(
                'リモートリソースは受け付けられません。' .
                'ローカルファイルのみ処理できます。'
            );
        }

        // 以降はパス文字列のみチェック
        if (!is_string($source)) return;

        // 2. 実パスを取得してパストラバーサルを防止
        $realPath = realpath($source);
        if ($realPath === false) {
            throw new StreamSecurityException(
                "パスが解決できません: {$source}"
            );
        }

        // 3. 許可ディレクトリ内かチェック
        if (!empty($this->allowedBasePaths)) {
            $allowed = false;
            foreach ($this->allowedBasePaths as $base) {
                if (str_starts_with($realPath, $base . DIRECTORY_SEPARATOR)) {
                    $allowed = true;
                    break;
                }
            }
            if (!$allowed) {
                throw new StreamSecurityException(
                    "許可されていないディレクトリへのアクセス: {$realPath}"
                );
            }
        }

        // 4. 拒否拡張子チェック
        $ext = strtolower(pathinfo($realPath, PATHINFO_EXTENSION));
        if (in_array($ext, $this->deniedExtensions, true)) {
            throw new StreamSecurityException(
                "拒否された拡張子: .{$ext}(ファイル: {$realPath})"
            );
        }
    }

    /**
     * バリデーションを通過した場合のみストリームを開いて返す
     *
     * @throws StreamSecurityException
     */
    public function openSafe(string $path, string $mode = 'r'): mixed
    {
        $this->validate($path);
        $fp = fopen($path, $mode);
        if ($fp === false) {
            throw new \RuntimeException("ファイルを開けません: {$path}");
        }
        return $fp;
    }
}

// 使用例
$validator = new SecureStreamValidator(
    allowedBasePaths: ['/tmp', '/var/app/uploads'],
    deniedExtensions: ['php', 'phtml', 'exe', 'sh']
);

$tests = [
    '/tmp/safe_data.csv',
    '/tmp/evil.php',
    'http://attacker.example.com/payload', // SSRF 試み
    '/etc/passwd',                         // パストラバーサル試み
];

foreach ($tests as $path) {
    try {
        $validator->validate($path);
        echo "OK: {$path}" . PHP_EOL;
    } catch (StreamSecurityException $e) {
        echo "NG: {$e->getMessage()}" . PHP_EOL;
    }
}

// 出力:
// OK: /tmp/safe_data.csv
// NG: 拒否された拡張子: .php(ファイル: /tmp/evil.php)
// NG: リモートリソースは受け付けられません。ローカルファイルのみ処理できます。
// NG: 許可されていないディレクトリへのアクセス: /etc/passwd

例3:キャッシュ戦略セレクター(CacheStrategySelector)

ローカルストリームにはインメモリキャッシュ、リモートストリームにはTTL付きファイルキャッシュと、stream_is_local() の結果に基づいてキャッシュ戦略を切り替えるクラスです。

<?php

class CacheStrategySelector
{
    /** @var array<string, string> インメモリキャッシュ(ローカル用) */
    private array $memoryCache = [];

    /** ファイルキャッシュのディレクトリ */
    private string $cacheDir;

    /** リモートリソースのキャッシュ有効秒数 */
    private int $remoteTtl;

    public function __construct(
        string $cacheDir  = '/tmp/stream_cache',
        int    $remoteTtl = 300
    ) {
        $this->cacheDir  = $cacheDir;
        $this->remoteTtl = $remoteTtl;

        if (!is_dir($cacheDir)) {
            mkdir($cacheDir, 0700, true);
        }
    }

    /**
     * ストリームまたはパスのコンテンツを取得する(キャッシュ戦略を自動選択)
     */
    public function get(string $source): string
    {
        if (stream_is_local($source)) {
            return $this->getFromLocal($source);
        }
        return $this->getFromRemote($source);
    }

    /**
     * ローカル:インメモリキャッシュを利用(プロセス内で再利用)
     */
    private function getFromLocal(string $path): string
    {
        $key = md5($path . filemtime($path));

        if (!isset($this->memoryCache[$key])) {
            $this->memoryCache[$key] = file_get_contents($path) ?: '';
        }

        return $this->memoryCache[$key];
    }

    /**
     * リモート:TTL付きファイルキャッシュを利用
     */
    private function getFromRemote(string $url): string
    {
        $cacheFile = $this->cacheDir . '/' . md5($url) . '.cache';

        // キャッシュが有効期限内なら返す
        if (
            file_exists($cacheFile)
            && (time() - filemtime($cacheFile)) < $this->remoteTtl
        ) {
            return file_get_contents($cacheFile) ?: '';
        }

        // キャッシュ期限切れ or 未キャッシュ → 取得してキャッシュ
        $context = stream_context_create([
            'http' => ['timeout' => 15, 'user_agent' => 'PHP CacheStrategySelector/1.0'],
        ]);
        $content = @file_get_contents($url, false, $context) ?: '';
        file_put_contents($cacheFile, $content);

        return $content;
    }

    /**
     * キャッシュの種別と状態を返す
     */
    public function status(string $source): array
    {
        $isLocal = stream_is_local($source);

        if ($isLocal) {
            $key    = is_file($source) ? md5($source . @filemtime($source)) : null;
            $cached = $key !== null && isset($this->memoryCache[$key]);
            return [
                'type'     => 'memory',
                'cached'   => $cached,
                'ttl'      => null,
                'strategy' => 'in-process memory cache(ファイル更新で自動無効化)',
            ];
        }

        $cacheFile = $this->cacheDir . '/' . md5($source) . '.cache';
        $exists    = file_exists($cacheFile);
        $age       = $exists ? (time() - filemtime($cacheFile)) : null;
        $valid     = $exists && $age < $this->remoteTtl;

        return [
            'type'      => 'file',
            'cached'    => $valid,
            'age'       => $age,
            'ttl'       => $this->remoteTtl,
            'remaining' => $valid ? ($this->remoteTtl - $age) : 0,
            'strategy'  => "ファイルキャッシュ(TTL: {$this->remoteTtl}秒)",
        ];
    }
}

// 使用例
$cache = new CacheStrategySelector(remoteTtl: 60);

// ローカルファイルのキャッシュ状態を確認
$localStatus = $cache->status('/tmp/data.txt');
echo "ローカル戦略: {$localStatus['strategy']}" . PHP_EOL;
echo "キャッシュ済み: " . ($localStatus['cached'] ? 'YES' : 'NO') . PHP_EOL;

// リモートURLのキャッシュ状態を確認
$remoteStatus = $cache->status('https://example.com/api/data.json');
echo PHP_EOL . "リモート戦略: {$remoteStatus['strategy']}" . PHP_EOL;
echo "キャッシュ済み: " . ($remoteStatus['cached'] ? 'YES' : 'NO') . PHP_EOL;

例4:ストリームコピーポリシークラス(StreamCopyPolicy)

コピー元がローカルかリモートかに応じて、コピー方法(直接ファイルコピー vs ストリーミングコピー)とバッファサイズを最適化するクラスです。

<?php

class StreamCopyResult
{
    public function __construct(
        public readonly bool   $success,
        public readonly int    $bytesCopied,
        public readonly string $method,
        public readonly float  $elapsedMs,
    ) {}

    public function __toString(): string
    {
        return sprintf(
            '%s | %s | %d bytes | %.2f ms',
            $this->success ? 'SUCCESS' : 'FAILED',
            $this->method,
            $this->bytesCopied,
            $this->elapsedMs
        );
    }
}

class StreamCopyPolicy
{
    public function __construct(
        private int $localBufferSize  = 1_048_576, // 1MB
        private int $remoteBufferSize = 16_384,    // 16KB
        private int $remoteTimeout    = 30,
    ) {}

    /**
     * ソースの種別に応じた最適な方法でデータをコピーする
     *
     * @param string|resource $source コピー元(パス文字列またはストリーム)
     * @param string|resource $dest   コピー先(パス文字列またはストリーム)
     */
    public function copy(mixed $source, mixed $dest): StreamCopyResult
    {
        $start    = microtime(true);
        $isLocal  = is_string($source) ? stream_is_local($source) : stream_is_local($source);
        $method   = $isLocal ? 'direct' : 'streaming';

        try {
            $bytes = $isLocal
                ? $this->copyLocal($source, $dest)
                : $this->copyRemote($source, $dest);

            $elapsed = (microtime(true) - $start) * 1000;
            return new StreamCopyResult(true, $bytes, $method, $elapsed);

        } catch (\Throwable $e) {
            $elapsed = (microtime(true) - $start) * 1000;
            return new StreamCopyResult(false, 0, $method, $elapsed);
        }
    }

    private function copyLocal(mixed $source, mixed $dest): int
    {
        // ローカル→ローカル:copy() または大バッファで高速コピー
        if (is_string($source) && is_string($dest)) {
            $size = filesize($source) ?: 0;
            copy($source, $dest);
            return $size;
        }

        $src = is_string($source) ? fopen($source, 'rb') : $source;
        $dst = is_string($dest)   ? fopen($dest,   'wb') : $dest;

        $bytes = stream_copy_to_stream($src, $dst, length: -1, offset: 0);

        if (is_string($source)) fclose($src);
        if (is_string($dest))   fclose($dst);

        return $bytes !== false ? $bytes : 0;
    }

    private function copyRemote(mixed $source, mixed $dest): int
    {
        $context = stream_context_create([
            'http' => ['timeout' => $this->remoteTimeout],
        ]);

        $src = is_string($source)
            ? fopen($source, 'rb', false, $context)
            : $source;

        if ($src === false) {
            throw new \RuntimeException("リモートソースを開けません: {$source}");
        }

        if (!is_string($source)) {
            stream_set_timeout($src, $this->remoteTimeout);
        }

        $dst   = is_string($dest) ? fopen($dest, 'wb') : $dest;
        $bytes = 0;

        while (!feof($src)) {
            $chunk = fread($src, $this->remoteBufferSize);
            if ($chunk === false) break;
            fwrite($dst, $chunk);
            $bytes += strlen($chunk);
        }

        if (is_string($source)) fclose($src);
        if (is_string($dest))   fclose($dst);

        return $bytes;
    }
}

// 使用例
$policy = new StreamCopyPolicy();

// ローカルコピー
$src = fopen('php://memory', 'w+');
fwrite($src, str_repeat('A', 1024));
rewind($src);

$dst    = fopen('php://memory', 'w+');
$result = $policy->copy($src, $dst);
echo "ローカルコピー: {$result}" . PHP_EOL;

fclose($src);
fclose($dst);

// パス文字列で判定確認
echo "stream_is_local('/tmp/file.txt'): "
    . (stream_is_local('/tmp/file.txt')          ? 'local' : 'remote') . PHP_EOL;
echo "stream_is_local('https://example.com/'): "
    . (stream_is_local('https://example.com/')   ? 'local' : 'remote') . PHP_EOL;

// 出力:
// ローカルコピー: SUCCESS | direct | 1024 bytes | 0.12 ms
// stream_is_local('/tmp/file.txt'): local
// stream_is_local('https://example.com/'): remote

例5:ローカル限定ストリームデコレーター(LocalOnlyStreamDecorator)

ローカルストリームにのみ適用可能な操作(fseek()ftruncate() など)を安全に提供するデコレータークラスです。リモートストリームに誤って適用しようとした際は明確な例外を投げます。

<?php

class RemoteStreamOperationException extends \LogicException {}

class LocalOnlyStreamDecorator
{
    private bool $isLocal;

    public function __construct(private $stream)
    {
        $this->isLocal = stream_is_local($stream);
    }

    /**
     * シーク(ローカルのみ)
     *
     * @throws RemoteStreamOperationException
     */
    public function seek(int $offset, int $whence = SEEK_SET): void
    {
        $this->assertLocal('seek');
        fseek($this->stream, $offset, $whence);
    }

    /**
     * 切り詰め(ローカルのみ)
     *
     * @throws RemoteStreamOperationException
     */
    public function truncate(int $size): void
    {
        $this->assertLocal('truncate');
        ftruncate($this->stream, $size);
    }

    /**
     * 先頭への巻き戻し(ローカルのみ)
     *
     * @throws RemoteStreamOperationException
     */
    public function rewind(): void
    {
        $this->assertLocal('rewind');
        rewind($this->stream);
    }

    /**
     * 現在位置を返す(ローカルのみ確実に機能する)
     *
     * @throws RemoteStreamOperationException
     */
    public function tell(): int
    {
        $this->assertLocal('tell');
        return ftell($this->stream);
    }

    /**
     * ローカル・リモート共通の読み取り
     */
    public function read(int $length): string|false
    {
        return fread($this->stream, $length);
    }

    /**
     * ローカル・リモート共通の書き込み
     */
    public function write(string $data): int|false
    {
        return fwrite($this->stream, $data);
    }

    /**
     * ローカル・リモート共通の行読み取り
     */
    public function readLine(int $length = 4096): string|false
    {
        return fgets($this->stream, $length);
    }

    public function isLocal(): bool { return $this->isLocal; }
    public function isEof(): bool   { return feof($this->stream); }

    private function assertLocal(string $operation): void
    {
        if (!$this->isLocal) {
            throw new RemoteStreamOperationException(
                "'{$operation}' はローカルストリームにのみ使用できます。" .
                "リモートストリームに対してはこの操作はサポートされていません。"
            );
        }
    }
}

// 使用例
// ローカルストリーム
$localFp  = fopen('php://memory', 'w+');
$local    = new LocalOnlyStreamDecorator($localFp);

$local->write("Hello, World!");
$local->rewind();
echo $local->read(5) . PHP_EOL; // Hello
echo "位置: " . $local->tell() . PHP_EOL; // 5
$local->seek(7);
echo $local->read(5) . PHP_EOL; // World
fclose($localFp);

// リモート風ストリーム(php://stdin で代用確認)
$remoteFp = fopen('php://memory', 'r');
$remote   = new LocalOnlyStreamDecorator($remoteFp);

// リモートなら seek は本来使えないが、php://memory はローカル扱い
// 実際のリモートストリームでの動作確認:
echo "isLocal: " . ($remote->isLocal() ? 'YES' : 'NO') . PHP_EOL;
fclose($remoteFp);

// HTTP ストリームの場合(実環境での確認):
// $http = fopen('http://example.com/', 'r');
// $dec  = new LocalOnlyStreamDecorator($http);
// try {
//     $dec->seek(100); // → RemoteStreamOperationException
// } catch (RemoteStreamOperationException $e) {
//     echo $e->getMessage();
// }

例6:マルチソースアグリゲーター(MultiSourceAggregator)

ローカルファイルとリモートURLが混在するソースリストから、それぞれ最適な方法でコンテンツを収集して統合するアグリゲータークラスです。

<?php

class SourceResult
{
    public function __construct(
        public readonly string  $source,
        public readonly bool    $isLocal,
        public readonly bool    $success,
        public readonly string  $content,
        public readonly ?string $error,
        public readonly float   $fetchMs,
    ) {}
}

class MultiSourceAggregator
{
    private array $results = [];

    public function __construct(
        private int $remoteTimeout  = 10,
        private int $maxRemoteBytes = 1_048_576, // 1MB
    ) {}

    /**
     * ソースリストから全コンテンツを収集する
     *
     * @param  string[] $sources ファイルパスまたはURL の配列
     * @return SourceResult[]
     */
    public function fetchAll(array $sources): array
    {
        $this->results = [];
        foreach ($sources as $source) {
            $this->results[] = $this->fetchOne($source);
        }
        return $this->results;
    }

    private function fetchOne(string $source): SourceResult
    {
        $isLocal = stream_is_local($source);
        $start   = microtime(true);

        try {
            $content = $isLocal
                ? $this->fetchLocal($source)
                : $this->fetchRemote($source);

            return new SourceResult(
                source:  $source,
                isLocal: $isLocal,
                success: true,
                content: $content,
                error:   null,
                fetchMs: (microtime(true) - $start) * 1000,
            );
        } catch (\Throwable $e) {
            return new SourceResult(
                source:  $source,
                isLocal: $isLocal,
                success: false,
                content: '',
                error:   $e->getMessage(),
                fetchMs: (microtime(true) - $start) * 1000,
            );
        }
    }

    private function fetchLocal(string $path): string
    {
        if (!file_exists($path)) {
            throw new \RuntimeException("ファイルが見つかりません: {$path}");
        }
        return file_get_contents($path) ?: '';
    }

    private function fetchRemote(string $url): string
    {
        $context = stream_context_create([
            'http' => [
                'timeout'    => $this->remoteTimeout,
                'user_agent' => 'PHP MultiSourceAggregator/1.0',
            ],
            'ssl'  => ['verify_peer' => true, 'verify_peer_name' => true],
        ]);

        $fp = @fopen($url, 'rb', false, $context);
        if ($fp === false) {
            throw new \RuntimeException("リモートリソースを開けません: {$url}");
        }

        $content = stream_get_contents($fp, length: $this->maxRemoteBytes);
        fclose($fp);
        return $content ?: '';
    }

    /**
     * 収集結果のサマリーを表示する
     */
    public function printSummary(): void
    {
        $local   = array_filter($this->results, fn($r) => $r->isLocal);
        $remote  = array_filter($this->results, fn($r) => !$r->isLocal);
        $success = array_filter($this->results, fn($r) => $r->success);

        echo "=== 収集サマリー ===" . PHP_EOL;
        echo sprintf(
            "合計: %d件(ローカル: %d件, リモート: %d件, 成功: %d件)\n",
            count($this->results),
            count($local),
            count($remote),
            count($success),
        );

        foreach ($this->results as $r) {
            $type   = $r->isLocal ? 'LOCAL ' : 'REMOTE';
            $status = $r->success ? 'OK  ' : 'FAIL';
            $size   = strlen($r->content);
            echo sprintf(
                "  [%s][%s] %s | %d bytes | %.2f ms%s\n",
                $type, $status,
                basename($r->source),
                $size,
                $r->fetchMs,
                $r->error ? " | エラー: {$r->error}" : ''
            );
        }
    }
}

// 使用例
$agg = new MultiSourceAggregator(remoteTimeout: 5);

// ローカルの一時ファイルを作成
file_put_contents('/tmp/data1.txt', 'ローカルデータ1');
file_put_contents('/tmp/data2.txt', 'ローカルデータ2');

$results = $agg->fetchAll([
    '/tmp/data1.txt',
    '/tmp/data2.txt',
    '/tmp/nonexistent.txt',           // 存在しないローカルファイル
    'https://example.com/',           // リモートURL
    'http://invalid.example.invalid', // 無効なURL
]);

$agg->printSummary();

// 出力例:
// === 収集サマリー ===
// 合計: 5件(ローカル: 3件, リモート: 2件, 成功: 3件)
//   [LOCAL ][OK  ] data1.txt | 17 bytes | 0.05 ms
//   [LOCAL ][OK  ] data2.txt | 17 bytes | 0.03 ms
//   [LOCAL ][FAIL] nonexistent.txt | 0 bytes | 0.02 ms | エラー: ファイルが見つかりません
//   [REMOTE][OK  ] example.com | 1256 bytes | 230.14 ms
//   [REMOTE][FAIL] invalid.example.invalid | 0 bytes | 5012.33 ms | エラー: ...

例7:ストリームポリシーエンジン(StreamPolicyEngine)

stream_is_local() の結果をトリガーにして、ロギング・圧縮・暗号化などのポリシーをローカル・リモートそれぞれに適用するポリシーエンジンです。

<?php

interface StreamPolicy
{
    public function apply(string $content, string $source): string;
    public function name(): string;
}

class CompressionPolicy implements StreamPolicy
{
    public function apply(string $content, string $source): string
    {
        $compressed = gzcompress($content, 6);
        return $compressed !== false ? $compressed : $content;
    }
    public function name(): string { return 'compression'; }
}

class LoggingPolicy implements StreamPolicy
{
    private array $log = [];

    public function apply(string $content, string $source): string
    {
        $this->log[] = sprintf(
            '[%s] source=%s bytes=%d',
            date('H:i:s'),
            $source,
            strlen($content)
        );
        return $content;
    }
    public function name(): string  { return 'logging'; }
    public function getLog(): array { return $this->log; }
}

class StreamPolicyEngine
{
    /** @var StreamPolicy[] ローカルストリーム用ポリシー */
    private array $localPolicies = [];

    /** @var StreamPolicy[] リモートストリーム用ポリシー */
    private array $remotePolicies = [];

    public function addLocalPolicy(StreamPolicy $policy): static
    {
        $this->localPolicies[] = $policy;
        return $this;
    }

    public function addRemotePolicy(StreamPolicy $policy): static
    {
        $this->remotePolicies[] = $policy;
        return $this;
    }

    /**
     * ソースに応じたポリシーを適用してコンテンツを返す
     */
    public function process(string $source, string $content): string
    {
        $policies = stream_is_local($source)
            ? $this->localPolicies
            : $this->remotePolicies;

        foreach ($policies as $policy) {
            $content = $policy->apply($content, $source);
        }

        return $content;
    }

    /**
     * 適用されるポリシー名の一覧を返す
     *
     * @return array{local: string[], remote: string[]}
     */
    public function getPolicyNames(): array
    {
        return [
            'local'  => array_map(fn($p) => $p->name(), $this->localPolicies),
            'remote' => array_map(fn($p) => $p->name(), $this->remotePolicies),
        ];
    }
}

// 使用例
$logging     = new LoggingPolicy();
$compression = new CompressionPolicy();

$engine = new StreamPolicyEngine();
$engine
    ->addLocalPolicy($logging)                 // ローカル: ロギングのみ
    ->addRemotePolicy($logging)                // リモート: ロギング +
    ->addRemotePolicy($compression);           //           圧縮(帯域節約)

$policyNames = $engine->getPolicyNames();
echo "ローカルポリシー: " . implode(', ', $policyNames['local'])  . PHP_EOL;
echo "リモートポリシー: " . implode(', ', $policyNames['remote']) . PHP_EOL;

// ローカル処理
$localContent  = $engine->process('/tmp/data.txt', '{"key":"value"}');
echo "ローカル処理後サイズ: " . strlen($localContent) . " bytes" . PHP_EOL;

// リモート処理(圧縮も適用)
$remoteContent = $engine->process('https://api.example.com/data', str_repeat('{"key":"value"}', 100));
echo "リモート処理後サイズ: " . strlen($remoteContent) . " bytes" . PHP_EOL;

foreach ($logging->getLog() as $entry) {
    echo "LOG: {$entry}" . PHP_EOL;
}

// 出力例:
// ローカルポリシー: logging
// リモートポリシー: logging, compression
// ローカル処理後サイズ: 15 bytes
// リモート処理後サイズ: 28 bytes(圧縮により縮小)
// LOG: [10:00:00] source=/tmp/data.txt bytes=15
// LOG: [10:00:00] source=https://api.example.com/data bytes=1500

関連する関数との比較

関数役割
stream_is_local()ストリーム / URL がローカルリソースかどうかを判定
stream_get_meta_data()ストリームのメタ情報(モード・タイムアウト・ラッパー種別など)を取得
stream_get_wrappers()利用可能なストリームラッパー名の一覧を返す
stream_get_transports()利用可能なソケットトランスポート名の一覧を返す
is_file()パスがファイルかどうかを判定(ローカルファイルシステムのみ)
filter_var($url, FILTER_VALIDATE_URL)文字列が有効なURLかどうかを検証

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

1. php:// ラッパーはローカル扱い

php://memoryphp://tempphp://stdin など php:// 系は全て true を返します。「ファイルシステム上のファイルか否か」ではなく、「ネットワーク越しのリソースか否か」の判定であることに注意してください。

var_dump(stream_is_local('php://memory')); // bool(true)
var_dump(stream_is_local('php://stdin'));  // bool(true)

2. カスタムラッパーの判定はラッパー定義次第

stream_wrapper_register() で登録したカスタムラッパーは、ラッパークラスの $context オプションの is_url フラグで判定されます。意図通りに判定されるよう、ラッパー実装時に明示的に設定しましょう。

3. セキュリティ用途では realpath() も併用する

stream_is_local() はローカルかどうかの判定のみで、パストラバーサルの防止は行いません。セキュリティ目的では realpath() と許可ディレクトリのチェックを合わせて行いましょう(例2参照)。

// ローカル確認 + パストラバーサル防止
if (stream_is_local($path) && str_starts_with(realpath($path), $allowedDir)) {
    // 安全なアクセス
}

4. 文字列とリソースの両方を受け付ける

stream_is_local() はファイルパス文字列・URL文字列・開いたストリームリソースのいずれも受け付けます。fopen() 前の事前チェックにも、開いた後の確認にも使えます。


まとめ

ポイント内容
基本動作ストリームまたはURLがローカルリソースなら true、リモートなら false
引数ストリームリソース または パス/URL 文字列
php://php://memory など php:// 系は全て true(ローカル扱い)
カスタムラッパーis_url フラグの設定によって判定が変わる
セキュリティrealpath() + 許可ディレクトリチェックと組み合わせて SSRF / パストラバーサルを防止
活用シーン処理の振り分け・キャッシュ戦略の選択・コピー最適化・セキュリティバリデーション・ポリシー適用など

stream_is_local() はシンプルな bool 返却ながら、ローカルとリモートを明確に区別してコードの安全性と可読性を高める重要な関数です。セキュリティチェック・パフォーマンス最適化・処理の振り分けなど、ストリームを扱うあらゆる場面で積極的に活用してください。

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