Skip to content

输出过滤器

概述

输出过滤器(Output Filters)是 PHP 输出缓冲的进阶功能。通过为 ob_start() 传入回调函数,可以在缓冲区内容发送之前对其进行自动处理。PHP 还提供了一些内置过滤器(如 ob_gzhandler),可以方便地实现内容压缩。自定义过滤器为内容转换、日志记录、安全过滤等提供了灵活的机制。

PHP 版本说明

  • ob_start 回调过滤器自 PHP 4.0.4 起可用
  • 内置过滤器 ob_gzhandler 自 PHP 4.0.4 起可用
  • ob_tidyhandler(需 Tidy 扩展)自 PHP 5.0 起可用
  • PHP 7.4+ 的箭头函数可以作为简洁的过滤器回调

基础概念

过滤器工作流程

输出内容 → 输出缓冲区 → [过滤器回调处理] → 最终输出

过滤器回调在缓冲区刷新时被调用,接收缓冲区内容作为参数,返回处理后的内容。

过滤器类型

类型说明示例
内置过滤器PHP 提供的标准过滤器ob_gzhandler(gzip 压缩)
自定义过滤器用户定义的回调函数大小写转换、HTML 净化
链式过滤器嵌套多个缓冲区实现多级过滤先压缩再加密
条件过滤器根据内容决定是否处理仅对 HTML 内容过滤

语法与代码

ob_start 基本回调过滤器

php
<?php

declare(strict_types=1);

// 使用回调作为过滤器
ob_start(function (string $buffer): string {
    return strtoupper($buffer);
});

echo "hello world";
echo "foo bar";

ob_end_flush();
// 输出: HELLO WORLD
// FOO BAR

箭头函数作为过滤器

php
<?php

declare(strict_types=1);

// 箭头函数:简洁的过滤器(PHP 7.4+)
ob_start(fn(string $buffer): string => strtoupper($buffer));
echo "hello from php 7.4+";
ob_end_flush(); // HELLO FROM PHP 7.4+

// 多个过滤操作
ob_start(fn(string $buffer): string => trim($buffer));
echo "  hello  ";
ob_end_flush(); // hello

// 组合操作
ob_start(function (string $buffer): string {
    $buffer = trim($buffer);
    $buffer = preg_replace('/\s+/', ' ', $buffer);
    return $buffer;
});

echo "  hello    world  \n  php  ";
ob_end_flush(); // hello world php

内置 gzip 过滤器

php
<?php

declare(strict_types=1);

// ob_gzhandler:自动 gzip 压缩输出
ob_start('ob_gzhandler');

header('Content-Encoding: gzip');

$largeContent = str_repeat('Hello, World! This is a test of gzip compression. ', 100);
echo $largeContent;

ob_end_flush();
// 浏览器会自动解压,用户看到的是原始内容
// 但传输过程中数据被压缩,节省带宽

嵌套过滤器实现链式处理

php
<?php

declare(strict_types=1);

// 第 1 层:转为大写
ob_start(fn(string $buffer): string => strtoupper($buffer));

// 第 2 层:替换文本
ob_start(fn(string $buffer): string => str_replace('world', 'PHP', $buffer));

echo "hello world";
// 第 2 层缓冲区内容:hello PHP(替换后)
// 第 1 层缓冲区内容:HELLO PHP(大写后)

// 关闭第 2 层(发送到第 1 层)
ob_end_flush();
// 关闭第 1 层(发送到浏览器)
ob_end_flush();
// 最终输出: HELLO PHP

过滤器回调参数详解

php
<?php

declare(strict_types=1);

// 过滤器回调的完整签名
// function callback(string $buffer, int $phase): string
// - $buffer: 缓冲区内容
// - $phase: 阶段(PHP_OUTPUT_HANDLER_START 或 PHP_OUTPUT_HANDLER_END)

$callCount = 0;

ob_start(function (string $buffer, int $phase): string {
    global $callCount;
    $callCount++;

    echo "[Filter called #{$callCount}, phase=" . ($phase === PHP_OUTPUT_HANDLER_START ? 'START' : 'END') . "]\n";

    return strtoupper($buffer);
});

echo "test content";

// ob_end_flush 时回调被调用
ob_end_flush();
// [Filter called #1, phase=START]
// TEST CONTENT

自定义内容过滤器

php
<?php

declare(strict_types=1);

// HTML 实体编码过滤器
$htmlFilter = fn(string $buffer): string => htmlspecialchars($buffer, ENT_QUOTES, 'UTF-8');

ob_start($htmlFilter);
echo '<script>alert("xss")</script>';
echo '<div class="test">Hello</div>';
echo htmlspecialchars("Hello & World");
ob_end_flush();
// 输出被编码为 HTML 实体

// 移除注释过滤器
ob_start(fn(string $buffer): string => preg_replace('/<!--.*?-->/s', '', $buffer));

$html = '<!-- This is a comment --><p>Visible content</p>';
echo $html;
ob_end_flush();
// 输出: <p>Visible content</p>

// 添加内容前缀/后缀过滤器
ob_start(function (string $buffer): string {
    $prefix = "<!-- Generated at " . date('Y-m-d H:i:s') . " -->\n";
    $suffix = "\n<!-- End of page -->";

    return $prefix . $buffer . $suffix;
});

echo "<html><body>Content</body></html>";
ob_end_flush();

详细说明

过滤器与 ob_get_level

php
<?php

declare(strict_types=1);

// 多层过滤器中每层独立工作
function createFilter(string $name): callable
{
    return function (string $buffer) use ($name): string {
        return "[{$name}] {$buffer}";
    };
}

ob_start(createFilter('filter1'));
ob_start(createFilter('filter2'));
ob_start(createFilter('filter3'));

echo "content";

ob_end_flush(); // [filter3] content → 发送到 filter2
ob_end_flush(); // [filter2] [filter3] content → 发送到 filter1
ob_end_flush(); // [filter1] [filter2] [filter3] content → 发送到浏览器

动态选择过滤器

php
<?php

declare(strict_types=1);

$format = 'json'; // 可选: json, html, text

$filter = match ($format) {
    'json' => fn(string $buffer): string => json_encode(['content' => $buffer], JSON_PRETTY_PRINT),
    'html' => fn(string $buffer): string => "<div class='content'>" . nl2br(htmlspecialchars($buffer)) . "</div>",
    'text' => fn(string $buffer): string => strip_tags($buffer),
    default => fn(string $buffer): string => $buffer,
};

ob_start($filter);
echo "Hello\nWorld\n<test>";
ob_end_flush();
// JSON 格式输出(当 $format = 'json')

过滤器中的错误处理

php
<?php

declare(strict_types=1);

ob_start(function (string $buffer): string {
    try {
        return mb_convert_encoding($buffer, 'UTF-8', 'auto');
    } catch (Throwable $e) {
        error_log("Filter error: " . $e->getMessage());
        return $buffer; // 出错时返回原始内容
    }
});

echo "Some content";
ob_end_flush();

实战示例

全局 HTML 净化过滤器

php
<?php

declare(strict_types=1);

class SecurityFilter
{
    public static function sanitize(string $buffer): string
    {
        // 移除危险的 HTML 标签
        $buffer = preg_replace('/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/si', '', $buffer);

        // 对 HTML 实体编码
        $buffer = htmlspecialchars($buffer, ENT_QUOTES | ENT_HTML5, 'UTF-8');

        // 移除 null 字节
        $buffer = str_replace(chr(0), '', $buffer);

        return $buffer;
    }

    public static function apply(): void
    {
        ob_start(fn(string $buffer): string => self::sanitize($buffer));
    }
}

// 注册全局过滤器
SecurityFilter::apply();

echo '<script>alert("xss")</script>';
echo '<p>Hello & World</p>';

ob_end_flush();
// 安全的输出

开发环境调试过滤器

php
<?php

declare(strict_types=1);

class DebugFilter
{
    private static float $startTime = 0;

    public static function start(): void
    {
        self::$startTime = microtime(true);

        ob_start(function (string $buffer): string {
            $elapsed = round((microtime(true) - self::$startTime) * 1000, 2);
            $size = strlen($buffer);
            $comment = "\n<!-- Rendered in {$elapsed}ms, size: {$size} bytes -->";

            return $buffer . $comment;
        });
    }
}

// 在应用初始化时启动
DebugFilter::start();

// ... 正常的页面输出 ...
echo "<html><body><h1>My Page</h1><p>Content here</p></body></html>";

// 脚本结束时缓冲区自动刷新

响应压缩过滤器

php
<?php

declare(strict_types=1);

function compressionFilter(string $buffer): string
{
    // 检查是否已经压缩
    if (str_contains(implode(' ', headers_list()), 'Content-Encoding')) {
        return $buffer;
    }

    $minLength = 1024; // 只压缩超过 1KB 的内容
    if (strlen($buffer) < $minLength) {
        return $buffer;
    }

    $compressed = gzencode($buffer, 6);

    if ($compressed === false) {
        return $buffer;
    }

    header('Content-Encoding: gzip');

    return $compressed;
}

ob_start('compressionFilter');

// 大量输出内容
$content = str_repeat('Lorem ipsum dolor sit amet, consectetur adipiscing elit. ', 50);
echo $content;

ob_end_flush();

注意事项

常见陷阱

  1. 过滤器中再次输出
php
<?php

declare(strict_types=1);

// 错误:在过滤器回调中 echo 会进入新的缓冲区或导致无限递归
// ob_start(function ($buffer) {
//     echo "prefix: "; // 不要这样做
//     return $buffer;
// });

// 正确:只返回处理后的内容
ob_start(fn(string $buffer): string => "prefix: " . $buffer);
  1. 过滤器修改了 header 后的内容
php
<?php

declare(strict_types=1);

// 如果在 ob_start 之后设置了 header
ob_start(fn(string $buffer): string => strtoupper($buffer));
header('Content-Type: text/plain');
echo "hello";
ob_end_flush();
// header 在过滤器处理之前已经发送,这是正常的
  1. 缓冲区大小限制
php
<?php

declare(strict_types=1);

// ob_start 的第二个参数控制缓冲区大小
// 超过大小后缓冲区会自动刷新
ob_start(null, 4096); // 4KB 缓冲区

// 第三个参数控制是否自动刷新
ob_start(null, 0, PHP_OUTPUT_HANDLER_FLUSHABLE);

最佳实践

1. 过滤器保持单一职责

php
<?php

declare(strict_types=1);

// 推荐:每个过滤器只做一件事
$trimFilter = fn(string $buffer): string => trim($buffer);
$encodeFilter = fn(string $buffer): string => htmlspecialchars($buffer, ENT_QUOTES, 'UTF-8');
$minifyFilter = fn(string $buffer): string => preg_replace('/\s+/', ' ', $buffer);

// 嵌套组合多个过滤器
ob_start($trimFilter);
ob_start($encodeFilter);
ob_start($minifyFilter);

echo "  content  with <tags> & entities  ";

ob_end_flush(); // minify → encode → trim → 输出
ob_end_flush();
ob_end_flush();

2. 使用 ob_get_clean + 手动处理

php
<?php

declare(strict_types=1);

// 有时手动处理更清晰
ob_start();
echo "<p>Hello</p>\n<p>World</p>\n<p>PHP</p>";

$html = ob_get_clean();
// 手动处理
$html = preg_replace('/<p>/', '<div class="paragraph">', $html);
$html = str_replace('</p>', '</div>', $html);

echo $html;

3. 生产环境考虑性能

php
<?php

declare(strict_types=1);

// 生产环境:只使用轻量过滤器
if (getenv('APP_ENV') === 'production') {
    ob_start(fn(string $buffer): string => trim($buffer));
} else {
    // 开发环境:使用调试过滤器
    ob_start(function (string $buffer): string {
        $time = date('Y-m-d H:i:s');
        return "<!-- [{$time}] Output size: " . strlen($buffer) . " bytes -->\n" . $buffer;
    });
}

参考链接