はじめに
PHPでストリームを扱う際、「このストリームはローカルファイルか、それともネットワーク越しのリソースか」を判定したい場面があります。セキュリティチェック・キャッシュ戦略の分岐・パフォーマンス最適化など、ローカル/リモートの区別が重要なケースは少なくありません。
stream_is_local() は、ストリームまたはURLがローカルリソースを指しているかどうかを bool で返す関数です。ファイルパス・ストリームリソース・URL文字列のいずれも受け付ける柔軟な判定関数で、ストリームの種別に応じた処理の分岐に活用できます。
関数の基本情報
| 項目 | 内容 |
|---|---|
| 関数名 | stream_is_local() |
| 対応バージョン | PHP 5.2.4 以降 |
| 返り値 | bool |
| カテゴリ | ストリーム関数 |
構文
stream_is_local(resource|string $stream): bool
パラメータ
| パラメータ | 型 | 説明 |
|---|---|---|
$stream | resource|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 フラグに基づいて判定します。
| ストリーム / URL | stream_is_local() | 理由 |
|---|---|---|
/path/to/file | true | ローカルファイルシステム |
file:///path/to/file | true | file:// ラッパーはローカル |
php://memory | true | php:// ラッパーはローカル |
php://temp | true | php:// ラッパーはローカル |
php://stdin | true | php:// ラッパーはローカル |
compress.zlib:///tmp/a.gz | true | ローカルファイルの圧縮ラッパー |
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://memory・php://temp・php://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 返却ながら、ローカルとリモートを明確に区別してコードの安全性と可読性を高める重要な関数です。セキュリティチェック・パフォーマンス最適化・処理の振り分けなど、ストリームを扱うあらゆる場面で積極的に活用してください。
