[PHP]stream_wrapper_unregister完全解説|ストリームラッパーを解除してモックや差し替えを実現する方法

PHP

はじめに

PHPの file_get_contents()fopen() は、内部的にストリームラッパーという仕組みを通じて動作しています。file://http://php:// といったスキームはそれぞれ対応する組み込みラッパーが処理しています。

stream_wrapper_unregister() は、これらの登録済みラッパー(組み込み・カスタム問わず)を一時的に解除する関数です。主な用途は:

  • ユニットテストで file:// をモックに差し替える前準備
  • http:// を差し替えて外部通信を遮断・制御する
  • カスタムラッパーを再登録する前の既存エントリ削除

シンプルな関数ですが、解除中はそのプロトコル全体が使えなくなるため、使い方を誤るとアプリケーション全体のファイルI/Oが壊れます。本記事では安全な使用パターンを中心に詳しく解説します。


関数の基本情報

項目内容
関数名stream_wrapper_unregister()
利用可能バージョンPHP 5.0以降
所属ストリーム関数(Stream Functions)
戻り値bool(成功時true、失敗時false
拡張機能不要(コア関数)

シグネチャ

stream_wrapper_unregister(string $protocol): bool

パラメータ

パラメータ説明
$protocolstring解除するプロトコルスキーム名(例:filehttpmyproto

戻り値の詳細

状況戻り値
解除成功true
指定プロトコルが登録されていないfalse
すでに解除済みfalse

3兄弟関数との関係

stream_wrapper_register()   → カスタムラッパーを新規登録
stream_wrapper_unregister() → 登録済みラッパーを解除 ← 本関数
stream_wrapper_restore()    → 解除した組み込みラッパーを復元

解除できるラッパーの種類

種類解除復元方法
組み込みラッパー(filehttpなど)✅ 可stream_wrapper_restore()
カスタムラッパー(register()で登録)✅ 可stream_wrapper_register()で再登録

登録済みラッパーの確認

解除前後の状態は stream_get_wrappers() で確認できます。

// 現在の全ラッパー一覧
print_r(stream_get_wrappers());

/*
Array
(
    [0] => https
    [1] => ftps
    [2] => compress.zlib
    [3] => php
    [4] => file
    [5] => glob
    [6] => data
    [7] => http
    [8] => ftp
    [9] => compress.bz2
    [10] => phar
)
*/

実践サンプル集(PHP 8.x対応)

サンプル1:カスタムラッパーの解除と再登録

最もシンプルなケース。stream_wrapper_register() で登録したラッパーを解除し、差し替える例です。

<?php
declare(strict_types=1);

class WrapperV1
{
    public mixed $context;
    private int $pos = 0;
    private string $buf = '';

    public function stream_open(string $path, string $mode, int $options, ?string &$op): bool
    {
        $this->buf = 'Version 1 のレスポンス';
        $this->pos = 0;
        return true;
    }
    public function stream_read(int $count): string
    {
        $c = substr($this->buf, $this->pos, $count);
        $this->pos += strlen($c);
        return $c;
    }
    public function stream_eof(): bool { return $this->pos >= strlen($this->buf); }
    public function stream_close(): void {}
    public function stream_stat(): array { return []; }
}

class WrapperV2
{
    public mixed $context;
    private int $pos = 0;
    private string $buf = '';

    public function stream_open(string $path, string $mode, int $options, ?string &$op): bool
    {
        $this->buf = 'Version 2 のレスポンス(改良版)';
        $this->pos = 0;
        return true;
    }
    public function stream_read(int $count): string
    {
        $c = substr($this->buf, $this->pos, $count);
        $this->pos += strlen($c);
        return $c;
    }
    public function stream_eof(): bool { return $this->pos >= strlen($this->buf); }
    public function stream_close(): void {}
    public function stream_stat(): array { return []; }
}

// V1を登録
stream_wrapper_register('demo', WrapperV1::class);
echo file_get_contents('demo://test') . "\n"; // Version 1 のレスポンス

// V2に差し替えたい → 先にunregisterが必要
$unregistered = stream_wrapper_unregister('demo');
echo "unregister: " . ($unregistered ? 'OK' : 'NG') . "\n";

stream_wrapper_register('demo', WrapperV2::class);
echo file_get_contents('demo://test') . "\n"; // Version 2 のレスポンス(改良版)

// 後始末
stream_wrapper_unregister('demo');

実行結果:

Version 1 のレスポンス
unregister: OK
Version 2 のレスポンス(改良版)

解説: stream_wrapper_register() は同名のプロトコルを二重登録できないため、差し替えたい場合は必ず先に stream_wrapper_unregister() を呼びます。


サンプル2:file:// のモック差し替えと安全な復元

テストでよく使われる、file:// をモックに差し替えるパターンです。

<?php
declare(strict_types=1);

class MockFileWrapper
{
    public mixed $context;

    private static array $fixtures = [];
    private string $buffer = '';
    private int $position = 0;
    private bool $writeable = false;
    private string $written = '';

    public static function set(string $path, string $content): void
    {
        self::$fixtures[$path] = $content;
    }

    public static function getWritten(string $path): string
    {
        return self::$fixtures[$path] ?? '';
    }

    public static function reset(): void
    {
        self::$fixtures = [];
    }

    public function stream_open(
        string $path,
        string $mode,
        int $options,
        ?string &$openedPath
    ): bool {
        $realPath = preg_replace('#^file://#', '', $path) ?? $path;

        if (str_contains($mode, 'w') || str_contains($mode, 'a')) {
            $this->writeable = true;
            self::$fixtures[$realPath] ??= '';
            $this->buffer = str_contains($mode, 'a')
                ? self::$fixtures[$realPath]
                : '';
        } else {
            if (!array_key_exists($realPath, self::$fixtures)) {
                return false;
            }
            $this->buffer = self::$fixtures[$realPath];
        }

        $this->written  = $realPath;
        $this->position = 0;
        return true;
    }

    public function stream_read(int $count): string
    {
        $chunk = substr($this->buffer, $this->position, $count);
        $this->position += strlen($chunk);
        return $chunk;
    }

    public function stream_write(string $data): int
    {
        self::$fixtures[$this->written] = substr(
            self::$fixtures[$this->written] ?? '', 0, $this->position
        ) . $data;
        $this->position += strlen($data);
        return strlen($data);
    }

    public function stream_eof(): bool { return $this->position >= strlen($this->buffer); }
    public function stream_tell(): int { return $this->position; }
    public function stream_close(): void {}

    public function stream_stat(): array
    {
        return ['size' => strlen($this->buffer)]
            + array_fill_keys(
                ['dev','ino','mode','nlink','uid','gid','rdev',
                 'atime','mtime','ctime','blksize','blocks'], 0
            );
    }

    public function url_stat(string $path, int $flags): array|false
    {
        $realPath = preg_replace('#^file://#', '', $path) ?? $path;
        if (!array_key_exists($realPath, self::$fixtures)) {
            return false;
        }
        return ['size' => strlen(self::$fixtures[$realPath]), 'mode' => 0100644]
            + array_fill_keys(
                ['dev','ino','nlink','uid','gid','rdev',
                 'atime','mtime','ctime','blksize','blocks'], 0
            );
    }
}

// --- テスト実行 ---

MockFileWrapper::set('/app/config.json', '{"debug":true,"env":"testing"}');
MockFileWrapper::set('/app/data.csv', "name,score\nAlice,95\nBob,87");

// file:// を解除してモックに差し替え
stream_wrapper_unregister('file');
stream_wrapper_register('file', MockFileWrapper::class);

try {
    // テスト対象コード(変更不要)
    $config = json_decode(file_get_contents('/app/config.json'), true);
    echo "env: {$config['env']}\n";         // env: testing

    $lines = file('/app/data.csv');
    echo "行数: " . count($lines) . "\n";  // 行数: 3

    echo "exists: " . (file_exists('/app/config.json') ? 'yes' : 'no') . "\n"; // exists: yes
    echo "missing: " . (file_exists('/app/missing.txt') ? 'yes' : 'no') . "\n"; // missing: no

    // 書き込みテスト
    file_put_contents('/app/output.txt', 'テスト出力');
    echo "written: " . MockFileWrapper::getWritten('/app/output.txt') . "\n";

} finally {
    // 確実に復元
    stream_wrapper_unregister('file');
    stream_wrapper_restore('file');
    MockFileWrapper::reset();
}

echo "復元確認: " . (in_array('file', stream_get_wrappers()) ? 'OK' : 'NG') . "\n";

実行結果:

env: testing
行数: 3
exists: yes
missing: no
written: テスト出力
復元確認: OK

解説: try-finally で復元を保証するのが鉄則です。例外が発生しても finally ブロックは必ず実行されるため、file:// が壊れたままになるリスクを排除できます。


サンプル3:解除状態の管理クラス(スタック型)

複数のラッパーを順序よく解除・復元する管理クラスです。

<?php
declare(strict_types=1);

/**
 * ストリームラッパーの解除をスタックで管理するクラス
 */
class WrapperUnregisterStack
{
    /** @var list<array{protocol: string, isBuiltin: bool, customClass: string|null}> */
    private array $stack = [];

    /** PHP組み込みラッパーの一覧 */
    private const BUILTIN = [
        'file', 'http', 'https', 'ftp', 'ftps',
        'php', 'zlib', 'data', 'glob', 'phar',
        'compress.zlib', 'compress.bz2',
    ];

    /**
     * ラッパーを解除してカスタムラッパーに差し替え
     */
    public function swap(string $protocol, string $newClass): self
    {
        if (!in_array($protocol, stream_get_wrappers(), true)) {
            throw new RuntimeException("'{$protocol}' は登録されていません");
        }

        $isBuiltin = in_array($protocol, self::BUILTIN, true);
        $this->stack[] = [
            'protocol'    => $protocol,
            'isBuiltin'   => $isBuiltin,
            'customClass' => $isBuiltin ? null : null, // カスタムは再登録不可
        ];

        stream_wrapper_unregister($protocol);
        stream_wrapper_register($protocol, $newClass);

        return $this;
    }

    /**
     * 解除のみ(差し替えなし)
     */
    public function unregister(string $protocol): self
    {
        if (!in_array($protocol, stream_get_wrappers(), true)) {
            throw new RuntimeException("'{$protocol}' は登録されていません");
        }

        $isBuiltin = in_array($protocol, self::BUILTIN, true);
        $this->stack[] = [
            'protocol'  => $protocol,
            'isBuiltin' => $isBuiltin,
            'customClass' => null,
        ];

        stream_wrapper_unregister($protocol);
        return $this;
    }

    /**
     * スタックを逆順に巻き戻す
     */
    public function rollback(): void
    {
        foreach (array_reverse($this->stack) as $entry) {
            $protocol = $entry['protocol'];

            // カスタムで差し替えていた場合は先に解除
            if (in_array($protocol, stream_get_wrappers(), true)) {
                stream_wrapper_unregister($protocol);
            }

            if ($entry['isBuiltin']) {
                stream_wrapper_restore($protocol);
            }
            // カスタムラッパーは再登録できないため復元しない
        }
        $this->stack = [];
    }

    public function depth(): int
    {
        return count($this->stack);
    }
}

// --- デモラッパー ---
class FakeHttpWrapper
{
    public mixed $context;
    private int $pos = 0;
    private string $buf = '{"status":"mocked"}';
    public function stream_open(string $p, string $m, int $o, ?string &$op): bool
    { $this->pos = 0; return true; }
    public function stream_read(int $n): string
    { $c = substr($this->buf, $this->pos, $n); $this->pos += strlen($c); return $c; }
    public function stream_eof(): bool { return $this->pos >= strlen($this->buf); }
    public function stream_close(): void {}
    public function stream_stat(): array { return []; }
}

// --- 使用例 ---
$stack = new WrapperUnregisterStack();

try {
    $stack->swap('http', FakeHttpWrapper::class)
          ->swap('https', FakeHttpWrapper::class);

    echo "スタック深さ: {$stack->depth()}\n"; // 2

    $result = file_get_contents('http://api.example.com/status');
    echo "レスポンス: {$result}\n"; // {"status":"mocked"}

} finally {
    $stack->rollback();
    echo "http登録: "  . (in_array('http',  stream_get_wrappers()) ? 'OK' : 'NG') . "\n";
    echo "https登録: " . (in_array('https', stream_get_wrappers()) ? 'OK' : 'NG') . "\n";
}

実行結果:

スタック深さ: 2
レスポンス: {"status":"mocked"}
http登録: OK
https登録: OK

解説: スタック構造で管理することで、複数ラッパーの解除を登録の逆順に確実に巻き戻せます。差し替え操作が増えるほどこのパターンが有効です。


サンプル4:解除状態をチェックしてから操作する

二重解除や未登録ラッパーの解除を防ぐ防御的コードのパターンです。

<?php
declare(strict_types=1);

/**
 * 安全にラッパーを解除するユーティリティ
 */
final class WrapperGuard
{
    /**
     * 登録確認付き解除
     */
    public static function unregister(string $protocol): bool
    {
        if (!self::isRegistered($protocol)) {
            trigger_error(
                "stream_wrapper_unregister: '{$protocol}' は登録されていません",
                E_USER_NOTICE
            );
            return false;
        }

        return stream_wrapper_unregister($protocol);
    }

    /**
     * 解除 → 新クラスで再登録(アトミック差し替え)
     */
    public static function swap(string $protocol, string $newClass): bool
    {
        if (!self::isRegistered($protocol)) {
            // 未登録なら直接登録
            return stream_wrapper_register($protocol, $newClass);
        }

        if (!stream_wrapper_unregister($protocol)) {
            return false;
        }

        return stream_wrapper_register($protocol, $newClass);
    }

    /**
     * 組み込みラッパーを安全に復元
     */
    public static function restore(string $protocol): bool
    {
        if (self::isRegistered($protocol)) {
            // 差し替え済みのカスタムクラスを先に解除
            stream_wrapper_unregister($protocol);
        }
        return stream_wrapper_restore($protocol);
    }

    public static function isRegistered(string $protocol): bool
    {
        return in_array($protocol, stream_get_wrappers(), true);
    }
}

// --- テスト ---

// ケース1:通常の解除
stream_wrapper_register('safe', new class {
    public mixed $context;
    public function stream_open(string $p, string $m, int $o, ?string &$op): bool { return true; }
    public function stream_read(int $n): string { return ''; }
    public function stream_eof(): bool { return true; }
    public function stream_close(): void {}
    public function stream_stat(): array { return []; }
}::class);

// ※ PHP 8.1以降は無名クラスをクラス名として渡すことはできないため
//   名前付きクラスを使うこと(上記はデモ目的の簡略表記)

// ケース2:二重解除の防止
stream_wrapper_unregister('ftp');
$result = WrapperGuard::unregister('ftp'); // 既に解除済み
echo "二重解除: " . ($result ? 'OK' : '防止(NG)') . "\n"; // 防止(NG)
stream_wrapper_restore('ftp');

// ケース3:登録状態の確認
echo "file登録: "  . (WrapperGuard::isRegistered('file')  ? 'yes' : 'no') . "\n"; // yes
echo "xyz登録: "   . (WrapperGuard::isRegistered('xyz')   ? 'yes' : 'no') . "\n"; // no

// ケース4:アトミックなswap
class AltHttpWrapper
{
    public mixed $context;
    private string $b = 'alt response';
    private int $p = 0;
    public function stream_open(string $path, string $mode, int $opt, ?string &$op): bool
    { $this->p = 0; return true; }
    public function stream_read(int $n): string
    { $c = substr($this->b, $this->p, $n); $this->p += strlen($c); return $c; }
    public function stream_eof(): bool { return $this->p >= strlen($this->b); }
    public function stream_close(): void {}
    public function stream_stat(): array { return []; }
}

WrapperGuard::swap('http', AltHttpWrapper::class);
echo file_get_contents('http://x') . "\n"; // alt response
WrapperGuard::restore('http');
echo "http復元: " . (WrapperGuard::isRegistered('http') ? 'OK' : 'NG') . "\n"; // OK

実行結果:

二重解除: 防止(NG)
file登録: yes
xyz登録: no
alt response
http復元: OK

解説: isRegistered() で事前チェックをはさむことで、二重解除によるWarningを防げます。swap() は解除と再登録をひとつのメソッドにまとめたアトミック操作です。


サンプル5:テストフレームワーク向けセットアップ・ティアダウン

PHPUnitなどでの setUp()tearDown() に組み込むパターンです。

<?php
declare(strict_types=1);

/**
 * ストリームラッパーのモックをテストケースに提供するトレイト
 */
trait MocksStreamWrappers
{
    private array $originalWrappers = [];
    private array $swappedProtocols = [];

    /**
     * setUp() から呼ぶ
     */
    protected function setUpStreamMocks(): void
    {
        $this->originalWrappers = stream_get_wrappers();
        $this->swappedProtocols = [];
    }

    /**
     * プロトコルをモッククラスで差し替える
     */
    protected function swapWrapper(string $protocol, string $mockClass): void
    {
        if (in_array($protocol, stream_get_wrappers(), true)) {
            stream_wrapper_unregister($protocol);
        }
        stream_wrapper_register($protocol, $mockClass);
        $this->swappedProtocols[] = $protocol;
    }

    /**
     * tearDown() から呼ぶ
     */
    protected function tearDownStreamMocks(): void
    {
        $builtins = [
            'file','http','https','ftp','ftps','php',
            'zlib','data','glob','phar','compress.zlib','compress.bz2',
        ];

        foreach ($this->swappedProtocols as $protocol) {
            if (in_array($protocol, stream_get_wrappers(), true)) {
                stream_wrapper_unregister($protocol);
            }
            if (in_array($protocol, $builtins, true)) {
                stream_wrapper_restore($protocol);
            }
        }

        $this->swappedProtocols = [];
    }
}

// --- サンプルのモッククラス ---
class FakeFileWrapper
{
    public mixed $context;
    private static array $store = [];
    private string $buf = '';
    private int $pos = 0;

    public static function put(string $path, string $content): void
    { self::$store[$path] = $content; }
    public static function clear(): void { self::$store = []; }

    public function stream_open(string $path, string $mode, int $opt, ?string &$op): bool
    {
        $p = preg_replace('#^file://#', '', $path) ?? $path;
        $this->buf = self::$store[$p] ?? '';
        $this->pos = 0;
        return isset(self::$store[$p]);
    }
    public function stream_read(int $n): string
    { $c = substr($this->buf, $this->pos, $n); $this->pos += strlen($c); return $c; }
    public function stream_eof(): bool { return $this->pos >= strlen($this->buf); }
    public function stream_close(): void {}
    public function stream_stat(): array { return ['size' => strlen($this->buf)]; }
    public function url_stat(string $path, int $flags): array|false
    {
        $p = preg_replace('#^file://#', '', $path) ?? $path;
        return isset(self::$store[$p])
            ? ['size' => strlen(self::$store[$p]), 'mode' => 0100644]
              + array_fill_keys(['dev','ino','nlink','uid','gid','rdev',
                                 'atime','mtime','ctime','blksize','blocks'], 0)
            : false;
    }
}

// --- テストクラスのシミュレーション ---
class MyServiceTest
{
    use MocksStreamWrappers;

    public function setUp(): void
    {
        $this->setUpStreamMocks();
        FakeFileWrapper::put('/etc/app/settings.ini', "[db]\nhost=localhost\nport=3306\n");
        $this->swapWrapper('file', FakeFileWrapper::class);
    }

    public function tearDown(): void
    {
        $this->tearDownStreamMocks();
        FakeFileWrapper::clear();
    }

    public function testReadSettings(): void
    {
        $raw = file_get_contents('/etc/app/settings.ini');
        assert(str_contains($raw, 'localhost'), 'host not found');
        echo "✅ testReadSettings: PASS\n";
    }

    public function testFileExists(): void
    {
        assert(file_exists('/etc/app/settings.ini') === true, 'should exist');
        assert(file_exists('/etc/app/missing.ini') === false, 'should not exist');
        echo "✅ testFileExists: PASS\n";
    }
}

$t = new MyServiceTest();

$t->setUp();
$t->testReadSettings();
$t->tearDown();

$t->setUp();
$t->testFileExists();
$t->tearDown();

// 全テスト後にfile://が復元されていることを確認
echo "file復元: " . (in_array('file', stream_get_wrappers()) ? 'OK' : 'NG') . "\n";

実行結果:

✅ testReadSettings: PASS
✅ testFileExists: PASS
file復元: OK

解説: トレイトを使うことで、複数のテストクラスでラッパーモック機能を共有できます。setUptearDownの対称性が保たれ、テスト間でのラッパー汚染を防げます。


サンプル6:解除のスコープを明示するスコープオブジェクト

解除期間をスコープで明示するRAAI(Resource Acquisition Is Initialization)パターンです。

<?php
declare(strict_types=1);

/**
 * with構文風に使えるラッパー差し替えスコープ
 */
class WrapperScope
{
    private array $swapped = [];

    private const BUILTIN = [
        'file','http','https','ftp','ftps','php',
        'zlib','data','glob','phar','compress.zlib','compress.bz2',
    ];

    public function __construct(array $swaps)
    {
        foreach ($swaps as $protocol => $class) {
            if (in_array($protocol, stream_get_wrappers(), true)) {
                stream_wrapper_unregister($protocol);
                $this->swapped[] = [
                    'protocol'  => $protocol,
                    'isBuiltin' => in_array($protocol, self::BUILTIN, true),
                ];
            }
            stream_wrapper_register($protocol, $class);
        }
    }

    public function __destruct()
    {
        foreach (array_reverse($this->swapped) as $entry) {
            if (in_array($entry['protocol'], stream_get_wrappers(), true)) {
                stream_wrapper_unregister($entry['protocol']);
            }
            if ($entry['isBuiltin']) {
                stream_wrapper_restore($entry['protocol']);
            }
        }
    }

    /**
     * クロージャにスコープを渡して実行するファクトリ
     */
    public static function run(array $swaps, callable $callback): mixed
    {
        $scope = new self($swaps);
        return $callback($scope);
    }
}

// --- デモ用モック ---
class JsonApiMock
{
    public mixed $context;
    private static array $routes = [
        'users'   => '[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]',
        'default' => '{"status":"ok"}',
    ];
    private string $buf = '';
    private int $pos = 0;

    public function stream_open(string $path, string $mode, int $opt, ?string &$op): bool
    {
        $key = basename(parse_url($path, PHP_URL_PATH) ?? '');
        $this->buf = self::$routes[$key] ?? self::$routes['default'];
        $this->pos = 0;
        return true;
    }
    public function stream_read(int $n): string
    { $c = substr($this->buf, $this->pos, $n); $this->pos += strlen($c); return $c; }
    public function stream_eof(): bool { return $this->pos >= strlen($this->buf); }
    public function stream_close(): void {}
    public function stream_stat(): array { return []; }
}

// スコープ型の使い方
$result = WrapperScope::run(
    ['http' => JsonApiMock::class, 'https' => JsonApiMock::class],
    function () {
        $users = json_decode(file_get_contents('https://api.example.com/users'), true);
        $status = json_decode(file_get_contents('https://api.example.com/status'), true);
        return ['users' => $users, 'status' => $status['status']];
    }
);

echo "ユーザー数: " . count($result['users']) . "\n"; // 2
echo "ステータス: {$result['status']}\n";            // ok

// スコープ終了後に復元確認
echo "http復元: "  . (in_array('http',  stream_get_wrappers()) ? 'OK' : 'NG') . "\n";
echo "https復元: " . (in_array('https', stream_get_wrappers()) ? 'OK' : 'NG') . "\n";

実行結果:

ユーザー数: 2
ステータス: ok
http復元: OK
https復元: OK

解説: コンストラクタで差し替え、デストラクタで復元するスコープオブジェクトパターンです。WrapperScope::run() を使うと、差し替えの開始・終了が明示的にコードで表現でき、復元漏れを構造的に防げます。


よくある落とし穴

① 解除中のファイル操作はすべてエラーになる

stream_wrapper_unregister('file');

// ❌ この間はローカルファイルアクセスが全滅する
file_get_contents('/etc/hostname');   // Warning: failed to open stream
file_put_contents('/tmp/out.txt', ''); // Warning
require '/path/to/SomeClass.php';     // Fatal error!

stream_wrapper_restore('file');

requireinclude もファイルI/Oなので、解除中に新たなファイルを読み込むコードを実行してはいけません。差し替え前にオートロードが必要なクラスはすべて読み込み済みにしておくことが重要です。

② try-finally なしでは例外時に復元されない

// ❌ 例外が発生すると file:// が壊れたままになる
stream_wrapper_unregister('file');
stream_wrapper_register('file', MockWrapper::class);
riskyOperation(); // 例外発生 →  以下が実行されない
stream_wrapper_unregister('file');
stream_wrapper_restore('file');

// ✅ try-finally で必ず復元
stream_wrapper_unregister('file');
stream_wrapper_register('file', MockWrapper::class);
try {
    riskyOperation();
} finally {
    stream_wrapper_unregister('file');
    stream_wrapper_restore('file');
}

③ 同名プロトコルへの二重登録はエラー

// ❌ unregisterせずにregisterすると失敗
stream_wrapper_register('myp', ClassA::class);
stream_wrapper_register('myp', ClassB::class); // false(失敗)

// ✅ 先にunregister
stream_wrapper_unregister('myp');
stream_wrapper_register('myp', ClassB::class); // OK

④ カスタムラッパーはrestore()で戻せない

stream_wrapper_register('myp', MyWrapper::class);
stream_wrapper_unregister('myp');

stream_wrapper_restore('myp'); // false(カスタムは復元不可)

// ✅ 再度registerする
stream_wrapper_register('myp', MyWrapper::class);

⑤ 解除前にstream_get_wrappers()で確認する

// 登録されていない可能性があるプロトコルを解除するとき
$protocol = 'myapp';

if (in_array($protocol, stream_get_wrappers(), true)) {
    stream_wrapper_unregister($protocol);
}

まとめ

ポイント内容
主な用途ラッパーの差し替え前処理・テストモックの準備・カスタムラッパーの更新
解除できるもの組み込み・カスタムラッパー問わずすべて
復元方法組み込みはstream_wrapper_restore()、カスタムはstream_wrapper_register()で再登録
安全な使い方try-finallyで復元を保証・スコープオブジェクトパターン
要注意解除中のrequire・二重登録エラー・例外時の復元漏れ

stream_wrapper_unregister() は単体で使うよりも、stream_wrapper_register()stream_wrapper_restore() と組み合わせて差し替え→使用→復元という一連のフローの中で使うことで真価を発揮します。try-finally またはスコープオブジェクトを活用して、常に復元を保証する設計を心がけましょう。


PHP 8.x / 執筆時点の最新安定版にて動作確認済み

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