はじめに
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_filter | resource | stream_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() と組み合わせることで、ストリーム処理の柔軟性が大きく広がります。
