[PHP]stream_filter_remove完全ガイド|ストリームフィルターを動的に取り外してI/O処理を柔軟にコントロールする

PHP

はじめに

PHPのストリームにフィルターをアタッチできる stream_filter_append()stream_filter_prepend() は広く知られていますが、「途中でフィルターを取り外す」機能を持つ stream_filter_remove() はあまり注目されていません。

stream_filter_remove() を使うと、一度アタッチしたフィルターをストリームを閉じることなく動的に取り外せます。「ファイルの前半は変換あり・後半は素通し」「特定の条件を満たしたらフィルターをオフにする」といった柔軟な I/O 制御が可能になります。


関数の基本情報

項目内容
関数名stream_filter_remove()
対応バージョンPHP 5.1.0 以降
返り値bool(成功時 true、失敗時 false
カテゴリストリーム関数

構文

stream_filter_remove(resource $stream_filter): bool

パラメータ

パラメータ説明
$stream_filterresourcestream_filter_append() または stream_filter_prepend() が返したフィルターリソース

返り値

  • true:フィルターの取り外しに成功
  • false:失敗(すでに取り外し済み、無効なリソースなど)

重要: stream_filter_append()stream_filter_prepend() の戻り値(フィルターリソース)を変数に保存しておかないと、後から stream_filter_remove() を呼び出せません。


基本的な使い方

<?php

// フィルターをアタッチ(戻り値を必ず変数に保存する)
$fp     = fopen('php://memory', 'w+');
$filter = stream_filter_append($fp, 'string.rot13', STREAM_FILTER_WRITE);

fwrite($fp, 'Hello'); // ROT13 が適用される → "Uryyb" として格納

// フィルターを取り外す
stream_filter_remove($filter);

fwrite($fp, 'World'); // フィルターなし → "World" がそのまま格納

rewind($fp);
echo fread($fp, 1024);
// 出力: UryyбWorld
fclose($fp);

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


例1:セクション別フォーマットライター(SectionWriter)

ファイルの「ヘッダー部」は大文字変換あり、「ボディ部」はそのまま書き込むように、セクションごとにフィルターを切り替えるライターです。

<?php

class UpperCaseFilter extends php_user_filter
{
    public function filter($in, $out, &$consumed, bool $closing): int
    {
        while ($bucket = stream_bucket_make_writeable($in)) {
            $bucket->data = strtoupper($bucket->data);
            $consumed += $bucket->datalen;
            stream_bucket_append($out, $bucket);
        }
        return PSFS_PASS_ON;
    }
}

stream_filter_register('str.uppercase', UpperCaseFilter::class);

class SectionWriter
{
    private $fp;
    private $filter = null;

    public function __construct(string $filePath)
    {
        $this->fp = fopen($filePath, 'w');
    }

    /** ヘッダー書き込み:大文字変換フィルターをON */
    public function writeHeader(string $text): void
    {
        if ($this->filter === null) {
            $this->filter = stream_filter_append($this->fp, 'str.uppercase', STREAM_FILTER_WRITE);
        }
        fwrite($this->fp, "=== {$text} ===" . PHP_EOL);
    }

    /** ボディ書き込み:フィルターをOFFにして素の文字列を書く */
    public function writeBody(string $text): void
    {
        if ($this->filter !== null) {
            stream_filter_remove($this->filter);
            $this->filter = null;
        }
        fwrite($this->fp, $text . PHP_EOL);
    }

    public function __destruct()
    {
        fclose($this->fp);
    }
}

// 使用例
$writer = new SectionWriter('/tmp/report.txt');
$writer->writeHeader('summary');      // → "=== SUMMARY ==="
$writer->writeBody('売上合計: 1,200,000円'); // → そのまま出力
$writer->writeHeader('detail');       // → "=== DETAIL ==="
$writer->writeBody('商品A: 800,000円');     // → そのまま出力

echo "書き込み完了" . PHP_EOL;
// /tmp/report.txt の内容:
// === SUMMARY ===
// 売上合計: 1,200,000円
// === DETAIL ===
// 商品A: 800,000円

例2:条件付きロギングフィルター(ConditionalLogger)

エラーが発生したときだけログ整形フィルターを有効にし、通常時は素のテキストで書き込む条件分岐ロガーです。

<?php

class ErrorTagFilter extends php_user_filter
{
    public function filter($in, $out, &$consumed, bool $closing): int
    {
        while ($bucket = stream_bucket_make_writeable($in)) {
            $timestamp    = date('Y-m-d H:i:s');
            $bucket->data = "[ERROR][{$timestamp}] " . trim($bucket->data) . PHP_EOL;
            $consumed    += $bucket->datalen;
            stream_bucket_append($out, $bucket);
        }
        return PSFS_PASS_ON;
    }
}

stream_filter_register('log.error_tag', ErrorTagFilter::class);

class ConditionalLogger
{
    private $fp;
    private $errorFilter = null;

    public function __construct(string $logPath)
    {
        $this->fp = fopen($logPath, 'a');
    }

    public function info(string $message): void
    {
        // エラーフィルターが有効なら取り外す
        $this->disableErrorFilter();
        fwrite($this->fp, "[INFO] {$message}" . PHP_EOL);
    }

    public function error(string $message): void
    {
        // エラーフィルターを有効にする
        $this->enableErrorFilter();
        fwrite($this->fp, $message);
    }

    private function enableErrorFilter(): void
    {
        if ($this->errorFilter === null) {
            $this->errorFilter = stream_filter_append(
                $this->fp,
                'log.error_tag',
                STREAM_FILTER_WRITE
            );
        }
    }

    private function disableErrorFilter(): void
    {
        if ($this->errorFilter !== null) {
            stream_filter_remove($this->errorFilter);
            $this->errorFilter = null;
        }
    }

    public function __destruct()
    {
        fclose($this->fp);
    }
}

// 使用例
$logger = new ConditionalLogger('/tmp/app.log');
$logger->info('アプリ起動');
$logger->error('DB接続タイムアウト');
$logger->info('リトライ中...');
$logger->error('最大リトライ回数超過');

// /tmp/app.log の内容:
// [INFO] アプリ起動
// [ERROR][2025-05-13 10:00:00] DB接続タイムアウト
// [INFO] リトライ中...
// [ERROR][2025-05-13 10:00:00] 最大リトライ回数超過

例3:フィルタースタック管理クラス(FilterStack)

複数のフィルターをスタック構造で管理し、push() でアタッチ・pop() で LIFO 順に取り外せる汎用ユーティリティです。

<?php

class TrimFilter extends php_user_filter
{
    public function filter($in, $out, &$consumed, bool $closing): int
    {
        while ($bucket = stream_bucket_make_writeable($in)) {
            $lines = explode("\n", $bucket->data);
            $bucket->data = implode("\n", array_map('trim', $lines));
            $consumed += $bucket->datalen;
            stream_bucket_append($out, $bucket);
        }
        return PSFS_PASS_ON;
    }
}

class PrefixFilter extends php_user_filter
{
    public function filter($in, $out, &$consumed, bool $closing): int
    {
        while ($bucket = stream_bucket_make_writeable($in)) {
            $prefix = $this->params ?? '> ';
            $lines  = explode("\n", rtrim($bucket->data, "\n"));
            $bucket->data = implode("\n", array_map(
                fn($l) => $l !== '' ? $prefix . $l : $l,
                $lines
            )) . "\n";
            $consumed += $bucket->datalen;
            stream_bucket_append($out, $bucket);
        }
        return PSFS_PASS_ON;
    }
}

stream_filter_register('text.trim',   TrimFilter::class);
stream_filter_register('text.prefix', PrefixFilter::class);

class FilterStack
{
    /** @var resource[] */
    private array $stack = [];
    private $stream;

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

    public function push(string $filterName, int $mode = STREAM_FILTER_ALL, mixed $params = null): void
    {
        $handle = stream_filter_append($this->stream, $filterName, $mode, $params);
        $this->stack[] = $handle;
    }

    /** スタックの末尾(最後に追加したフィルター)を取り外す */
    public function pop(): bool
    {
        if (empty($this->stack)) {
            return false;
        }
        $handle = array_pop($this->stack);
        return stream_filter_remove($handle);
    }

    /** 全フィルターを取り外す */
    public function clear(): void
    {
        while (!empty($this->stack)) {
            $this->pop();
        }
    }

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

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

$stack->push('text.trim',   STREAM_FILTER_WRITE);         // フィルター1: トリム
$stack->push('text.prefix', STREAM_FILTER_WRITE, '>> '); // フィルター2: プレフィックス

fwrite($fp, "  Hello World  \n");
fwrite($fp, "  PHP Streams  \n");

// フィルター2(prefix)だけ取り外す
$stack->pop();

fwrite($fp, "  No Prefix Line  \n");

rewind($fp);
echo fread($fp, 4096);
// 出力:
// >> Hello World
// >> PHP Streams
// No Prefix Line

fclose($fp);

例4:一時的なマスキングフィルター(SensitiveDataWriter)

クレジットカード番号やパスワードなどの機密情報を含む行を書き込む際だけフィルターを適用し、完了後に取り外すことでログへの漏洩を防ぐライターです。

<?php

class MaskSensitiveFilter extends php_user_filter
{
    private array $patterns = [
        '/\b(\d{4})[- ]?(\d{4})[- ]?(\d{4})[- ]?(\d{4})\b/' => '$1-****-****-$4', // クレジットカード
        '/("password"\s*:\s*")[^"]+(")/i'                    => '$1****$2',          // JSONパスワード
        '/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/' => '***@***.***',  // メールアドレス
    ];

    public function filter($in, $out, &$consumed, bool $closing): int
    {
        while ($bucket = stream_bucket_make_writeable($in)) {
            foreach ($this->patterns as $pattern => $replacement) {
                $bucket->data = preg_replace($pattern, $replacement, $bucket->data);
            }
            $consumed += $bucket->datalen;
            stream_bucket_append($out, $bucket);
        }
        return PSFS_PASS_ON;
    }
}

stream_filter_register('security.mask', MaskSensitiveFilter::class);

class SensitiveDataWriter
{
    private $fp;

    public function __construct(string $logPath)
    {
        $this->fp = fopen($logPath, 'a');
    }

    /**
     * 機密情報を含む可能性があるデータを書き込む
     * フィルターを適用して書いた後、即座に取り外す
     */
    public function writeSensitive(string $data): void
    {
        $filter = stream_filter_append($this->fp, 'security.mask', STREAM_FILTER_WRITE);
        fwrite($this->fp, $data . PHP_EOL);
        stream_filter_remove($filter);
    }

    /** 通常データはそのまま書き込む */
    public function write(string $data): void
    {
        fwrite($this->fp, $data . PHP_EOL);
    }

    public function __destruct()
    {
        fclose($this->fp);
    }
}

// 使用例
$writer = new SensitiveDataWriter('/tmp/audit.log');

$writer->write('ユーザー登録処理を開始');
$writer->writeSensitive('カード番号: 4111 1111 1111 1234, メール: user@example.com');
$writer->writeSensitive('{"password": "s3cr3tP@ss", "user": "admin"}');
$writer->write('処理完了');

// /tmp/audit.log の内容:
// ユーザー登録処理を開始
// カード番号: 4111-****-****-1234, メール: ***@***.***
// {"password": "****", "user": "admin"}
// 処理完了

例5:フィルタートグルクラス(FilterToggle)

同じフィルターを ON/OFF 切り替えながら使い回せるクラスです。圧縮・暗号化・変換などを動的に有効化・無効化する場面で便利です。

<?php

class Rot13Filter extends php_user_filter
{
    public function filter($in, $out, &$consumed, bool $closing): int
    {
        while ($bucket = stream_bucket_make_writeable($in)) {
            $bucket->data = str_rot13($bucket->data);
            $consumed    += $bucket->datalen;
            stream_bucket_append($out, $bucket);
        }
        return PSFS_PASS_ON;
    }
}

stream_filter_register('str.rot13', Rot13Filter::class);

class FilterToggle
{
    private $stream;
    private string $filterName;
    private int    $mode;
    private mixed  $params;
    private        $handle = null;

    public function __construct($stream, string $filterName, int $mode = STREAM_FILTER_ALL, mixed $params = null)
    {
        $this->stream     = $stream;
        $this->filterName = $filterName;
        $this->mode       = $mode;
        $this->params     = $params;
    }

    public function enable(): void
    {
        if ($this->handle === null) {
            $this->handle = stream_filter_append(
                $this->stream,
                $this->filterName,
                $this->mode,
                $this->params
            );
        }
    }

    public function disable(): void
    {
        if ($this->handle !== null) {
            stream_filter_remove($this->handle);
            $this->handle = null;
        }
    }

    public function isEnabled(): bool
    {
        return $this->handle !== null;
    }
}

// 使用例
$fp     = fopen('php://memory', 'w+');
$toggle = new FilterToggle($fp, 'str.rot13', STREAM_FILTER_WRITE);

$toggle->enable();
fwrite($fp, "Secret Message\n"); // ROT13 適用

$toggle->disable();
fwrite($fp, "Plain Text\n");     // 素通し

$toggle->enable();
fwrite($fp, "Another Secret\n"); // ROT13 適用

rewind($fp);
echo fread($fp, 4096);
// 出力:
// Frperg Zrffntr
// Plain Text
// Nabgure Frperg

fclose($fp);

例6:パイプラインプロセッサー(StreamPipeline)

ストリームに複数フィルターをアタッチし、処理完了後に全フィルターを一括解除するパイプラインクラスです。バッチ処理やETL(抽出・変換・ロード)処理に適しています。

<?php

class StripTagsFilter extends php_user_filter
{
    public function filter($in, $out, &$consumed, bool $closing): int
    {
        while ($bucket = stream_bucket_make_writeable($in)) {
            $bucket->data = strip_tags($bucket->data);
            $consumed    += $bucket->datalen;
            stream_bucket_append($out, $bucket);
        }
        return PSFS_PASS_ON;
    }
}

class NormalizeSpaceFilter extends php_user_filter
{
    public function filter($in, $out, &$consumed, bool $closing): int
    {
        while ($bucket = stream_bucket_make_writeable($in)) {
            $bucket->data = preg_replace('/\s+/', ' ', trim($bucket->data));
            $consumed    += $bucket->datalen;
            stream_bucket_append($out, $bucket);
        }
        return PSFS_PASS_ON;
    }
}

stream_filter_register('html.strip_tags',     StripTagsFilter::class);
stream_filter_register('text.normalize_space', NormalizeSpaceFilter::class);

class StreamPipeline
{
    /** @var resource[] */
    private array $handles = [];
    private $stream;

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

    public function addFilter(string $name, int $mode = STREAM_FILTER_READ, mixed $params = null): static
    {
        $this->handles[] = stream_filter_append($this->stream, $name, $mode, $params);
        return $this;
    }

    /**
     * クロージャを実行し、終了後に全フィルターを取り外す
     */
    public function run(callable $callback): void
    {
        try {
            $callback($this->stream);
        } finally {
            $this->teardown();
        }
    }

    private function teardown(): void
    {
        foreach (array_reverse($this->handles) as $handle) {
            stream_filter_remove($handle);
        }
        $this->handles = [];
    }
}

// 使用例
$html = "<h1>  PHPの  <em>ストリーム</em>  </h1>\n<p>  フィルターは  <strong>便利</strong>  です  </p>";

$fp = fopen('php://memory', 'w+');
fwrite($fp, $html);
rewind($fp);

$pipeline = new StreamPipeline($fp);
$pipeline
    ->addFilter('html.strip_tags',      STREAM_FILTER_READ)
    ->addFilter('text.normalize_space', STREAM_FILTER_READ)
    ->run(function ($stream) {
        while (($line = fgets($stream)) !== false) {
            echo trim($line) . PHP_EOL;
        }
    });

// パイプライン終了後、フィルターは全て取り外される
// 出力:
// PHPの ストリーム
// フィルターは 便利 です

fclose($fp);

例7:フィルター有効期限管理クラス(TimedFilter)

一定時間または一定バイト数を処理したらフィルターを自動的に取り外す、有効期限付きフィルター管理クラスです。

<?php

class WordCountFilter extends php_user_filter
{
    public static int $totalWords = 0;

    public function filter($in, $out, &$consumed, bool $closing): int
    {
        while ($bucket = stream_bucket_make_writeable($in)) {
            self::$totalWords += str_word_count($bucket->data);
            $consumed += $bucket->datalen;
            stream_bucket_append($out, $bucket);
        }
        return PSFS_PASS_ON;
    }
}

stream_filter_register('stat.word_count', WordCountFilter::class);

class TimedFilter
{
    private        $handle     = null;
    private float  $expireAt;
    private int    $maxBytes;
    private int    $bytesRead  = 0;
    private $stream;
    private string $filterName;
    private int    $mode;

    /**
     * @param resource $stream
     * @param string   $filterName
     * @param int      $ttlSeconds  フィルターの有効秒数(0 = 無制限)
     * @param int      $maxBytes    最大処理バイト数(0 = 無制限)
     */
    public function __construct(
        $stream,
        string $filterName,
        int    $ttlSeconds = 0,
        int    $maxBytes   = 0,
        int    $mode       = STREAM_FILTER_READ
    ) {
        $this->stream     = $stream;
        $this->filterName = $filterName;
        $this->mode       = $mode;
        $this->expireAt   = $ttlSeconds > 0 ? microtime(true) + $ttlSeconds : PHP_FLOAT_MAX;
        $this->maxBytes   = $maxBytes;

        $this->handle = stream_filter_append($this->stream, $this->filterName, $this->mode);
    }

    /**
     * 読み込みのたびに有効期限をチェックし、必要なら取り外す
     */
    public function read(int $length = 1024): string|false
    {
        $this->checkExpiry();

        $data = fread($this->stream, $length);
        if ($data !== false) {
            $this->bytesRead += strlen($data);
            $this->checkExpiry();
        }
        return $data;
    }

    private function checkExpiry(): void
    {
        if ($this->handle === null) return;

        $expired  = microtime(true) >= $this->expireAt;
        $oversize = $this->maxBytes > 0 && $this->bytesRead >= $this->maxBytes;

        if ($expired || $oversize) {
            stream_filter_remove($this->handle);
            $this->handle = null;
            // 理由をログ出力(実用時はロガーへ)
            $reason = $expired ? '時間切れ' : 'バイト数超過';
            error_log("TimedFilter: フィルター '{$this->filterName}' を取り外しました({$reason})");
        }
    }

    public function isActive(): bool
    {
        return $this->handle !== null;
    }
}

// 使用例:最大 50 バイト処理したらワード集計フィルターを取り外す
$text = str_repeat("PHP stream filter is powerful. ", 5);

$fp = fopen('php://memory', 'r+');
fwrite($fp, $text);
rewind($fp);

$timed = new TimedFilter($fp, 'stat.word_count', ttlSeconds: 0, maxBytes: 50);

while (!feof($fp)) {
    $chunk = $timed->read(32);
    if ($chunk === false) break;
}

fclose($fp);

echo "集計ワード数: " . WordCountFilter::$totalWords . PHP_EOL;
// 出力例: 集計ワード数: (最初の50バイト分のワード数)
echo "フィルターアクティブ: " . ($timed->isActive() ? 'YES' : 'NO') . PHP_EOL;
// 出力: フィルターアクティブ: NO

関連する関数との比較

関数役割
stream_filter_append()フィルターをストリームの末尾に追加し、リソースを返す
stream_filter_prepend()フィルターをストリームの先頭に追加し、リソースを返す
stream_filter_remove()アタッチ済みフィルターをストリームから取り外す
stream_filter_register()カスタムフィルタークラスをシステムに登録する
stream_get_filters()使用可能なフィルター名を一覧取得する

append と prepend の取り外し順序

複数のフィルターがアタッチされている場合、取り外す順序は独立しており、どのフィルターのリソースを渡しても個別に取り外せます。ただし、データの流れへの影響を考慮した順序で取り外すことを推奨します。

$f1 = stream_filter_append($fp, 'filter.one',   STREAM_FILTER_WRITE);
$f2 = stream_filter_append($fp, 'filter.two',   STREAM_FILTER_WRITE);
$f3 = stream_filter_append($fp, 'filter.three', STREAM_FILTER_WRITE);

// 任意の順で個別に取り外せる
stream_filter_remove($f2); // filter.two だけ取り外す
// この後のデータは filter.one → filter.three の順で処理される

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

1. 戻り値を必ず変数に保存する

// NG:取り外せなくなる
stream_filter_append($fp, 'my.filter', STREAM_FILTER_WRITE);

// OK:後で取り外せる
$filter = stream_filter_append($fp, 'my.filter', STREAM_FILTER_WRITE);
stream_filter_remove($filter);

2. finally ブロックで確実に取り外す

例外が発生してもフィルターが残存しないよう、finally で取り外すパターンが堅牢です。

$filter = stream_filter_append($fp, 'my.filter', STREAM_FILTER_WRITE);
try {
    // ストリームへの書き込み処理
    processStream($fp);
} finally {
    stream_filter_remove($filter);
}

3. ストリームを閉じると全フィルターは自動解除される

fclose() 時にアタッチ中のフィルターはすべて自動的に取り外されます。stream_filter_remove() を呼ばずにストリームを閉じても問題はありませんが、同じストリームを継続して使う場合は明示的に取り外すことが重要です。

4. 取り外し後のハンドルは無効になる

$filter = stream_filter_append($fp, 'my.filter', STREAM_FILTER_WRITE);
stream_filter_remove($filter);

// NG:すでに取り外し済みのリソースを再使用してはいけない
stream_filter_remove($filter); // false を返す

まとめ

ポイント内容
基本的な役割stream_filter_append() / prepend() が返したリソースをストリームから取り外す
戻り値の保存取り外しには append() / prepend() の戻り値(リソース)が必須
個別取り外し複数フィルターのうち特定の1つだけを選んで取り外せる
安全な解除finally ブロックで取り外すと例外時も安全
自動解除fclose() 時には全フィルターが自動的に取り外される
活用シーンセクション別フォーマット、条件付きロギング、一時的な機密マスキング、パイプライン処理など

stream_filter_remove() は「フィルターを付けたまま使い続ける」だけでなく、「必要なタイミングだけ有効にする」という動的な I/O 制御を可能にします。stream_filter_register() / stream_filter_append() と組み合わせることで、ストリーム処理の柔軟性が大きく広がります。

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