输出过滤器
概述
输出过滤器(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();注意事项
常见陷阱
- 过滤器中再次输出
php
<?php
declare(strict_types=1);
// 错误:在过滤器回调中 echo 会进入新的缓冲区或导致无限递归
// ob_start(function ($buffer) {
// echo "prefix: "; // 不要这样做
// return $buffer;
// });
// 正确:只返回处理后的内容
ob_start(fn(string $buffer): string => "prefix: " . $buffer);- 过滤器修改了 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 在过滤器处理之前已经发送,这是正常的- 缓冲区大小限制
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;
});
}