[PHP]stream_filter_prepend完全解説|ストリームフィルタを先頭に挿入して優先処理する方法を実践サンプルで理解する

PHP

stream_filter_prependとは?

stream_filter_prepend() は、既存のストリームリソースにフィルタをチェーンの先頭に挿入する関数です。

stream_filter_append() がフィルタを末尾に追加するのに対し、stream_filter_prepend()先頭に割り込ませる点が最大の違いです。ストリームフィルタはチェーン順にデータが流れるため、「既に追加されているフィルタよりも前に処理を挟みたい」という場面で使います。

たとえば、文字コード変換フィルタが追加済みのストリームに対して、その前段階として「改行コード正規化フィルタ」を差し込みたい場合、prepend を使うことで挿入順を意識したチェーン構築が可能になります。


基本構文

stream_filter_prepend(
    resource $stream,
    string   $filtername,
    int      $read_write = PSFS_FEED_ME,
    mixed    $params     = null
): resource|false
引数説明
$streamresourceフィルタを追加するストリームリソース
$filternamestringフィルタ名(string.toupperconvert.iconv.* など)
$read_writeint読み取り・書き込みどちらに適用するかのフラグ
$paramsmixedフィルタに渡す追加パラメータ
戻り値resource|falseフィルタリソース(成功時)、失敗時は false

$read_write フラグ

定数説明
STREAM_FILTER_READ1読み取り時のみ適用
STREAM_FILTER_WRITE2書き込み時のみ適用
STREAM_FILTER_ALL3読み書き両方に適用

append と prepend の違い

<?php
// append で追加した場合のチェーン順
$stream = fopen('/tmp/test.txt', 'w');
stream_filter_append($stream, 'string.toupper', STREAM_FILTER_WRITE); // チェーン: [toupper]
stream_filter_append($stream, 'string.rot13',   STREAM_FILTER_WRITE); // チェーン: [toupper → rot13]
// データの流れ: 入力 → toupper → rot13 → ファイル

// prepend で追加した場合のチェーン順
$stream2 = fopen('/tmp/test2.txt', 'w');
stream_filter_append($stream2,  'string.toupper', STREAM_FILTER_WRITE); // チェーン: [toupper]
stream_filter_prepend($stream2, 'string.rot13',   STREAM_FILTER_WRITE); // チェーン: [rot13 → toupper]
// データの流れ: 入力 → rot13 → toupper → ファイル
関数挿入位置データが流れる順
stream_filter_append()チェーンの末尾後から追加したフィルタが最後に実行
stream_filter_prepend()チェーンの先頭後から追加したフィルタが最初に実行

基本的な使い方

<?php
// ファイルへの書き込み時に rot13 → toupper の順で処理(prependで先頭挿入)
$stream = fopen('/tmp/prepend_test.txt', 'w');

stream_filter_append($stream,  'string.toupper', STREAM_FILTER_WRITE);
// チェーン: [toupper]

stream_filter_prepend($stream, 'string.rot13',   STREAM_FILTER_WRITE);
// チェーン: [rot13 → toupper]  ← rot13 が先頭に割り込んだ

fwrite($stream, "hello prepend\n");
fclose($stream);

echo file_get_contents('/tmp/prepend_test.txt');
// hello → rot13 → URYYB → toupper → URYYB (すでに大文字なので変化なし)
// 実際の出力: URYYB CERCRAW
<?php
// 読み取り時のフィルタを prepend で先頭に挿入
file_put_contents('/tmp/read_src.txt', "HELLO WORLD\n");

$stream = fopen('/tmp/read_src.txt', 'r');
stream_filter_append($stream,  'string.rot13',   STREAM_FILTER_READ);
stream_filter_prepend($stream, 'string.tolower', STREAM_FILTER_READ);
// チェーン: [tolower → rot13]

echo fread($stream, 1024);
// HELLO WORLD → tolower → hello world → rot13 → uryyb jbeyq
fclose($stream);

実践クラスサンプル

サンプル1:フィルタチェーンの順序を管理するビルダークラス

appendprepend を組み合わせてフィルタの実行順を明示的に制御します。

<?php

class FilterChainManager
{
    private resource $stream;
    /** @var array<string, resource> */
    private array $filters = [];
    private array $order   = [];

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

    public function append(string $name, int $readWrite = STREAM_FILTER_WRITE, mixed $params = null): static
    {
        $filter = stream_filter_append($this->stream, $name, $readWrite, $params);
        if ($filter === false) {
            throw new RuntimeException("フィルタ追加失敗(append): {$name}");
        }
        $this->filters[$name] = $filter;
        $this->order[]        = "[末尾] {$name}";
        return $this;
    }

    public function prepend(string $name, int $readWrite = STREAM_FILTER_WRITE, mixed $params = null): static
    {
        $filter = stream_filter_prepend($this->stream, $name, $readWrite, $params);
        if ($filter === false) {
            throw new RuntimeException("フィルタ追加失敗(prepend): {$name}");
        }
        $this->filters[$name] = $filter;
        array_unshift($this->order, "[先頭] {$name}");
        return $this;
    }

    public function remove(string $name): bool
    {
        if (!isset($this->filters[$name])) {
            return false;
        }
        $result = stream_filter_remove($this->filters[$name]);
        unset($this->filters[$name]);
        $this->order = array_filter($this->order, fn($o) => !str_contains($o, $name));
        return $result;
    }

    public function printChain(): void
    {
        echo "フィルタチェーン(実行順):\n";
        foreach (array_values($this->order) as $i => $entry) {
            echo "  " . ($i + 1) . ". {$entry}\n";
        }
    }

    public function getStream(): resource
    {
        return $this->stream;
    }
}

// 使用例
$stream  = fopen('/tmp/chain_manager.txt', 'w');
$manager = new FilterChainManager($stream);

$manager
    ->append('string.toupper')   // チェーン: [toupper]
    ->append('string.rot13')     // チェーン: [toupper → rot13]
    ->prepend('string.tolower'); // チェーン: [tolower → toupper → rot13]

$manager->printChain();

fwrite($stream, "Hello Chain Manager\n");
fclose($stream);

echo "\n出力: " . file_get_contents('/tmp/chain_manager.txt');
// hello chain manager → toupper → HELLO CHAIN MANAGER → rot13 → URYYB PUNVA ZNATRE

サンプル2:文字コード変換の前処理として改行コード正規化を挿入するクラス

既存の文字コード変換フィルタの前段に改行コード正規化フィルタを prepend で差し込みます。

<?php

/**
 * 改行コードを LF に統一するカスタムフィルタ
 */
class NewlineNormalizerFilter extends php_user_filter
{
    public function filter($in, $out, &$consumed, bool $closing): int
    {
        while ($bucket = stream_bucket_make_writeable($in)) {
            // CR+LF → LF、CR単体 → LF に統一
            $bucket->data  = str_replace(["\r\n", "\r"], "\n", $bucket->data);
            $consumed     += $bucket->datalen;
            stream_bucket_append($out, $bucket);
        }
        return PSFS_PASS_ON;
    }
}

stream_filter_register('newline.normalize', NewlineNormalizerFilter::class);

class EncodingPipeline
{
    private resource $stream;

    public function __construct(string $path, string $fromEnc, string $toEnc)
    {
        $this->stream = fopen($path, 'wb');

        // まず文字コード変換フィルタを追加
        stream_filter_append(
            $this->stream,
            "convert.iconv.{$fromEnc}.{$toEnc}",
            STREAM_FILTER_WRITE
        );

        // その前段として改行コード正規化を prepend で挿入
        // チェーン: [newline.normalize → convert.iconv.*]
        stream_filter_prepend($this->stream, 'newline.normalize', STREAM_FILTER_WRITE);
    }

    public function write(string $data): void
    {
        fwrite($this->stream, $data);
    }

    public function close(): void
    {
        fclose($this->stream);
    }
}

// 使用例:CRLF混じりのUTF-8テキストをSJISに変換して保存
$pipeline = new EncodingPipeline('/tmp/normalized_sjis.txt', 'UTF-8', 'SJIS');
$pipeline->write("Windowsの改行コード\r\nCR+LFが混在\rCR単体も含む\n通常LF\n");
$pipeline->close();

echo "ファイルサイズ: " . filesize('/tmp/normalized_sjis.txt') . " バイト\n";
echo "変換・正規化完了\n";

サンプル3:読み取りストリームに復号フィルタを先頭に割り込ませるクラス

すでにフィルタが適用されているストリームに対して、復号処理を最初に実行させます。

<?php

/**
 * 簡易XOR難読化フィルタ(デモ用)
 */
class XorObfuscationFilter extends php_user_filter
{
    private int $key = 0x42;

    public function onCreate(): bool
    {
        $this->key = is_int($this->params) ? $this->params : 0x42;
        return true;
    }

    public function filter($in, $out, &$consumed, bool $closing): int
    {
        while ($bucket = stream_bucket_make_writeable($in)) {
            $data = $bucket->data;
            $len  = strlen($data);
            for ($i = 0; $i < $len; $i++) {
                $data[$i] = chr(ord($data[$i]) ^ $this->key);
            }
            $bucket->data  = $data;
            $consumed     += $bucket->datalen;
            stream_bucket_append($out, $bucket);
        }
        return PSFS_PASS_ON;
    }
}

stream_filter_register('xor.obfuscate', XorObfuscationFilter::class);

class SecureStreamReader
{
    /**
     * 既存フィルタのあるストリームの先頭にXOR復号を挿入する
     */
    public static function addDecryptFirst(resource $stream, int $key = 0x42): void
    {
        // prepend で先頭に差し込む → XOR復号 → その後の処理
        stream_filter_prepend($stream, 'xor.obfuscate', STREAM_FILTER_READ, $key);
    }
}

// 使用例:XOR難読化したデータを書き込んでおく
$key  = 0x5A;
$data = "秘密のログデータ: user=admin, action=login\n";

// 書き込み時にXORエンコード
$wStream = fopen('/tmp/xor_data.bin', 'wb');
stream_filter_append($wStream, 'xor.obfuscate', STREAM_FILTER_WRITE, $key);
fwrite($wStream, $data);
fclose($wStream);

// 読み取りストリームを開き、大文字変換フィルタを追加してから
// 先頭にXOR復号を prepend で割り込ませる
$rStream = fopen('/tmp/xor_data.bin', 'rb');
stream_filter_append($rStream,  'string.toupper',  STREAM_FILTER_READ);
// チェーン: [toupper]

SecureStreamReader::addDecryptFirst($rStream, $key);
// チェーン: [xor.obfuscate → toupper]  ← 先頭に復号が割り込んだ

echo "復号+大文字変換:\n";
echo fread($rStream, 1024);
fclose($rStream);

サンプル4:ログ出力パイプラインにタイムスタンプフィルタを先頭挿入するクラス

既存のフィルタチェーンの前段にタイムスタンプ付与フィルタを動的に追加します。

<?php

/**
 * 各行の先頭にタイムスタンプを付加するカスタムフィルタ
 */
class TimestampPrependFilter extends php_user_filter
{
    private string $buffer  = '';
    private string $format  = '';

    public function onCreate(): bool
    {
        $this->format = is_string($this->params) ? $this->params : 'Y-m-d H:i:s';
        $this->buffer = '';
        return true;
    }

    public function filter($in, $out, &$consumed, bool $closing): int
    {
        while ($bucket = stream_bucket_make_writeable($in)) {
            $this->buffer .= $bucket->data;
            $consumed     += $bucket->datalen;
        }

        $lines  = explode("\n", $this->buffer);
        $this->buffer = array_pop($lines);
        $output = '';

        foreach ($lines as $line) {
            $ts      = date($this->format);
            $output .= "[{$ts}] {$line}\n";
        }

        if ($closing && $this->buffer !== '') {
            $ts           = date($this->format);
            $output      .= "[{$ts}] {$this->buffer}";
            $this->buffer = '';
        }

        if ($output !== '') {
            $newBucket = stream_bucket_new($this->stream, $output);
            stream_bucket_append($out, $newBucket);
        }

        return PSFS_PASS_ON;
    }
}

stream_filter_register('log.timestamp', TimestampPrependFilter::class);

class DynamicLogPipeline
{
    private resource $stream;
    private bool     $timestampEnabled = false;

    public function __construct(string $path)
    {
        $this->stream = fopen($path, 'a');
        // まず大文字変換(ダミーの既存フィルタ)を追加
        // 実際のユースケースでは別の変換フィルタが設定済みの状況を想定
    }

    public function enableTimestamp(string $format = 'H:i:s'): void
    {
        if (!$this->timestampEnabled) {
            // 先頭に差し込むことで、タイムスタンプを最初に付与してから後続処理へ
            stream_filter_prepend($this->stream, 'log.timestamp', STREAM_FILTER_WRITE, $format);
            $this->timestampEnabled = true;
        }
    }

    public function log(string $message): void
    {
        fwrite($this->stream, $message . "\n");
    }

    public function close(): void
    {
        fclose($this->stream);
    }
}

// 使用例
@unlink('/tmp/dynamic_log.txt');
$pipeline = new DynamicLogPipeline('/tmp/dynamic_log.txt');

$pipeline->log('タイムスタンプなしのログ行');

// 途中からタイムスタンプを有効化(prependで先頭に挿入)
$pipeline->enableTimestamp('H:i:s');

$pipeline->log('タイムスタンプ付きのログ行A');
$pipeline->log('タイムスタンプ付きのログ行B');
$pipeline->close();

echo file_get_contents('/tmp/dynamic_log.txt');

サンプル5:Base64デコード後にzlib伸長するチェーンを prepend で構築するクラス

受信データが「Base64エンコード済みzlib圧縮データ」の場合に、prepend で正しい順序のフィルタチェーンを構築します。

<?php

class CompressedBase64Reader
{
    /**
     * Base64エンコード + zlib圧縮されたデータをデコードして読み取る
     *
     * フィルタチェーン: [base64-decode → zlib.inflate]
     * 読み取り時に base64デコードしてから zlib伸長する
     */
    public static function read(string $encodedData): string
    {
        $stream = fopen('php://memory', 'r+b');
        fwrite($stream, $encodedData);
        rewind($stream);

        // まず zlib.inflate を append で追加
        stream_filter_append($stream, 'zlib.inflate', STREAM_FILTER_READ);
        // チェーン: [zlib.inflate]

        // Base64デコードを prepend で先頭に挿入
        // チェーン: [base64-decode → zlib.inflate]
        stream_filter_prepend($stream, 'convert.base64-decode', STREAM_FILTER_READ);

        $result = stream_get_contents($stream);
        fclose($stream);

        return $result !== false ? $result : '';
    }

    /**
     * データを zlib圧縮 → Base64エンコードして保存する
     *
     * フィルタチェーン: [zlib.deflate → base64-encode]
     */
    public static function write(string $data): string
    {
        $stream = fopen('php://memory', 'r+b');

        stream_filter_append($stream, 'zlib.deflate',         STREAM_FILTER_WRITE, 6);
        stream_filter_append($stream, 'convert.base64-encode', STREAM_FILTER_WRITE);

        fwrite($stream, $data);
        // フィルタをフラッシュさせるためにストリームを閉じる前に取得
        fclose($stream);

        // 再度メモリストリームで同じ処理(fclose後に読めないため)
        $out = fopen('php://memory', 'r+b');
        stream_filter_append($out, 'zlib.deflate',          STREAM_FILTER_WRITE, 6);
        stream_filter_append($out, 'convert.base64-encode', STREAM_FILTER_WRITE);
        fwrite($out, $data);

        // 書き込みフィルタを経由した内容は直接 stream_get_contents では読めないため
        // 一時ファイルを使ってフラッシュを確実に行う
        $tmp = tempnam(sys_get_temp_dir(), 'php_stream_');
        $dst = fopen($tmp, 'wb');
        stream_filter_append($dst, 'zlib.deflate',          STREAM_FILTER_WRITE, 6);
        stream_filter_append($dst, 'convert.base64-encode', STREAM_FILTER_WRITE);
        fwrite($dst, $data);
        fclose($dst);

        $encoded = file_get_contents($tmp);
        unlink($tmp);

        return $encoded;
    }
}

// 使用例
$original = str_repeat("PHPストリームフィルタのprepend活用サンプルデータ\n", 50);
$encoded  = CompressedBase64Reader::write($original);

echo "元サイズ:     " . strlen($original) . " バイト\n";
echo "変換後サイズ: " . strlen($encoded)  . " バイト\n";

$decoded = CompressedBase64Reader::read($encoded);
echo "復元一致:     " . ($decoded === $original ? 'YES' : 'NO') . "\n";
echo "先頭50文字:   " . mb_substr($decoded, 0, 50) . "\n";

サンプル6:読み取りストリームに検証フィルタを先頭挿入するクラス

データ読み取り時に最初にチェックサム検証を実行し、改ざんを検知します。

<?php

/**
 * 読み取りデータのCRC32チェックサムを計算して記録するフィルタ
 */
class ChecksumFilter extends php_user_filter
{
    public static int $lastChecksum = 0;
    private string    $buffer       = '';

    public function filter($in, $out, &$consumed, bool $closing): int
    {
        while ($bucket = stream_bucket_make_writeable($in)) {
            $this->buffer .= $bucket->data;
            $consumed     += $bucket->datalen;
            stream_bucket_append($out, $bucket);
        }

        if ($closing) {
            self::$lastChecksum = crc32($this->buffer);
        }

        return PSFS_PASS_ON;
    }
}

stream_filter_register('data.checksum', ChecksumFilter::class);

class VerifiedStreamReader
{
    private resource $stream;
    private int      $expectedChecksum;

    public function __construct(string $path, int $expectedChecksum)
    {
        $this->stream           = fopen($path, 'rb');
        $this->expectedChecksum = $expectedChecksum;

        // 既存フィルタ(大文字変換など)が先に追加されていると想定
        stream_filter_append($this->stream, 'string.toupper', STREAM_FILTER_READ);
        // チェーン: [toupper]

        // チェックサム検証を prepend で最初に差し込む
        // チェーン: [checksum → toupper]
        // → 元データのチェックサムを計算してから大文字変換
        stream_filter_prepend($this->stream, 'data.checksum', STREAM_FILTER_READ);
    }

    public function readAndVerify(): string|false
    {
        $content = stream_get_contents($this->stream);

        if (ChecksumFilter::$lastChecksum !== $this->expectedChecksum) {
            echo "⚠ チェックサム不一致!改ざんの可能性があります\n";
            echo "  期待値: {$this->expectedChecksum}\n";
            echo "  実際値: " . ChecksumFilter::$lastChecksum . "\n";
            return false;
        }

        echo "✓ チェックサム検証OK (CRC32: {$this->expectedChecksum})\n";
        return $content;
    }

    public function close(): void
    {
        fclose($this->stream);
    }
}

// 使用例
$data     = "検証対象のストリームデータです。改ざん検知テスト。\n";
$checksum = crc32($data);
file_put_contents('/tmp/verified_data.txt', $data);

echo "期待チェックサム: {$checksum}\n\n";

$reader  = new VerifiedStreamReader('/tmp/verified_data.txt', $checksum);
$content = $reader->readAndVerify();
$reader->close();

if ($content !== false) {
    echo "\n読み取り内容(大文字変換済み):\n{$content}";
}

サンプル7:フィルタを動的に組み替えるストリームプロセッサクラス

処理モードに応じて prependappendremove を組み合わせ、フィルタチェーンを動的に再構成します。

<?php

class DynamicStreamProcessor
{
    private resource $stream;
    /** @var array<string, resource> */
    private array $activeFilters = [];

    public function __construct(string $path, string $mode = 'w')
    {
        $this->stream = fopen($path, $mode);
    }

    public function insertFirst(string $filter, int $rw = STREAM_FILTER_WRITE, mixed $params = null): void
    {
        $resource = stream_filter_prepend($this->stream, $filter, $rw, $params);
        if ($resource === false) {
            throw new RuntimeException("prepend失敗: {$filter}");
        }
        // 先頭に挿入したのでactiveFiltersの先頭に記録
        $this->activeFilters = [$filter => $resource] + $this->activeFilters;
        echo "先頭に挿入: {$filter}\n";
    }

    public function insertLast(string $filter, int $rw = STREAM_FILTER_WRITE, mixed $params = null): void
    {
        $resource = stream_filter_append($this->stream, $filter, $rw, $params);
        if ($resource === false) {
            throw new RuntimeException("append失敗: {$filter}");
        }
        $this->activeFilters[$filter] = $resource;
        echo "末尾に追加: {$filter}\n";
    }

    public function remove(string $filter): void
    {
        if (!isset($this->activeFilters[$filter])) {
            return;
        }
        stream_filter_remove($this->activeFilters[$filter]);
        unset($this->activeFilters[$filter]);
        echo "削除: {$filter}\n";
    }

    public function write(string $data): void
    {
        fwrite($this->stream, $data);
    }

    public function printStatus(): void
    {
        $names = array_keys($this->activeFilters);
        echo "現在のチェーン: "
           . (empty($names) ? '(なし)' : implode(' → ', $names))
           . "\n";
    }

    public function close(): void
    {
        fclose($this->stream);
    }
}

// 使用例
@unlink('/tmp/dynamic_proc.txt');
$proc = new DynamicStreamProcessor('/tmp/dynamic_proc.txt', 'w');

// フェーズ1:大文字変換のみ
$proc->insertLast('string.toupper');
$proc->printStatus();
$proc->write("phase one input\n");

// フェーズ2:大文字変換の前にROT13を先頭挿入
$proc->insertFirst('string.rot13');
$proc->printStatus();
$proc->write("phase two input\n");

// フェーズ3:ROT13を削除して大文字変換だけに戻す
$proc->remove('string.rot13');
$proc->printStatus();
$proc->write("phase three input\n");

$proc->close();

echo "\n=== 出力ファイル ===\n";
echo file_get_contents('/tmp/dynamic_proc.txt');

prepend が特に有効な場面

シナリオ理由
既存フィルタの前段に前処理を追加したいappend だとチェーン末尾になってしまう
サードパーティライブラリが追加したフィルタよりもに処理したい自分のフィルタを最初に実行させられる
圧縮データの復号 → 伸長のように順序が厳密に決まっている正しいデコード順をチェーンで保証できる
フィルタチェーンを後から再構成したい追加後もチェーン先頭への割り込みが可能

関連関数との比較

関数用途
stream_filter_prepend()フィルタをチェーン先頭に挿入
stream_filter_append()フィルタをチェーン末尾に追加
stream_filter_remove()追加したフィルタを削除
stream_filter_register()カスタムフィルタを登録
stream_get_filters()利用可能なフィルタ名一覧を取得

まとめ

項目内容
関数名stream_filter_prepend()
分類ストリームフィルタ関数
PHP バージョンPHP 4.3.0以上
戻り値フィルタリソース(成功時)、false(失敗時)
主な用途既存フィルタチェーンの先頭への優先フィルタ挿入

stream_filter_prepend()stream_filter_append() と対になる関数で、フィルタを先頭に差し込む点が唯一の違いです。「後から追加したフィルタを最初に実行させたい」「既存チェーンの前段に前処理を割り込ませたい」という場面で威力を発揮します。append との使い分けを意識することで、圧縮・エンコード・変換・検証など複数の処理を正しい順序でパイプライン化できるようになります。

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