PHP header_register_callback() 函数

header_register_callback() 函数用于注册一个在 PHP 开始发送输出之前调用的回调函数。

该函数允许你在 HTTP 头信息发送之前执行最后的处理,例如添加、修改或验证头信息。这在某些高级场景下非常有用,比如在输出开始前动态设置头信息。

注意:该函数在 PHP 5.4.0 及更高版本中可用。回调函数将在所有头信息准备好之后,但在任何输出发送之前被调用。

语法

header_register_callback ( callable $callback ) : bool

参数

参数 类型 描述
$callback callable 必需的。在输出开始前调用的回调函数。
  • 可以是函数名的字符串
  • 可以是类方法数组 array($object, 'methodName')
  • 可以是静态方法数组 array('ClassName', 'methodName')
  • 可以是匿名函数(闭包)

返回值

返回值 描述
true 成功注册回调函数。
false 注册失败。可能的原因包括:
  • 回调函数不可调用
  • 头信息已经发送
  • 已经注册了回调函数(只能注册一个)

示例

示例 1:基本使用 - 注册简单回调函数

以下示例展示了如何注册一个简单的回调函数。

<?php
// 定义一个回调函数
function header_callback() {
    echo "回调函数被调用 - 输出即将开始\n";

    // 在回调函数中仍然可以设置头信息
    if (!headers_sent()) {
        header("X-Callback-Header: Called");
    }
}

// 注册回调函数
if (header_register_callback('header_callback')) {
    echo "回调函数注册成功\n";
} else {
    echo "回调函数注册失败\n";
}

// 设置一些头信息
header("Content-Type: text/plain");
header("X-Powered-By: PHP/7.4");

echo "这是页面内容\n";

// 输出结果:
// 回调函数注册成功
// 回调函数被调用 - 输出即将开始
// 这是页面内容
?>

示例 2:使用匿名函数(闭包)

使用匿名函数作为回调函数。

<?php
// 使用匿名函数作为回调
$result = header_register_callback(function() {
    echo "匿名回调函数被调用\n";

    // 记录回调被调用的时间
    if (!headers_sent()) {
        header("X-Callback-Time: " . microtime(true));
    }

    // 可以检查已设置的头部
    $headers = headers_list();
    echo "已设置 " . count($headers) . " 个头部信息\n";
});

if ($result) {
    echo "匿名回调函数注册成功\n";
}

// 设置一些头部
header("Content-Type: text/html");
header("Cache-Control: no-cache");

echo "<h1>页面内容</h1>\n";
?>

示例 3:使用类方法作为回调

使用类方法作为回调函数。

<?php
class HeaderManager {
    private static $callCount = 0;

    public static function callback() {
        self::$callCount++;
        echo "HeaderManager 回调被调用,第 " . self::$callCount . " 次\n";

        // 添加一些安全头部
        if (!headers_sent()) {
            header("X-Frame-Options: SAMEORIGIN");
            header("X-Content-Type-Options: nosniff");
            header("X-XSS-Protection: 1; mode=block");
        }
    }

    public static function getCallCount() {
        return self::$callCount;
    }
}

// 注册静态方法作为回调
header_register_callback(['HeaderManager', 'callback']);

// 或者注册对象方法
// $manager = new HeaderManager();
// header_register_callback([$manager, 'callback']);

header("Content-Type: text/plain");
echo "主要输出内容\n";

// 注意:回调函数只会在输出开始时调用一次
echo "回调函数被调用了 " . HeaderManager::getCallCount() . " 次\n";
?>

示例 4:在回调中验证和修改头部

在回调函数中验证和修改已经设置的头部信息。

<?php
function validate_headers() {
    $headers = headers_list();
    $foundContentType = false;

    foreach ($headers as $header) {
        if (stripos($header, 'Content-Type:') === 0) {
            $foundContentType = true;

            // 确保 Content-Type 包含字符集
            if (stripos($header, 'charset=') === false) {
                // 移除旧的 Content-Type
                header_remove('Content-Type');
                // 添加带字符集的 Content-Type
                header("Content-Type: text/html; charset=UTF-8");
            }
        }
    }

    // 如果没有 Content-Type,添加一个默认的
    if (!$foundContentType) {
        header("Content-Type: text/html; charset=UTF-8");
    }

    // 添加安全头部
    header("X-Frame-Options: DENY");
    header("Referrer-Policy: strict-origin-when-cross-origin");
}

// 注册验证回调
header_register_callback('validate_headers');

// 模拟一些头部设置(可能忘记设置字符集)
header("Content-Type: text/html");
// 或者完全不设置 Content-Type
// header("X-Custom: SomeValue");

echo "页面内容\n";

// 实际发送的头部将包含正确的 Content-Type 和安全头部
?>

示例 5:只能注册一个回调函数

演示只能注册一个回调函数,后续注册会失败。

<?php
function first_callback() {
    echo "第一个回调函数\n";
}

function second_callback() {
    echo "第二个回调函数\n";
}

// 注册第一个回调函数
if (header_register_callback('first_callback')) {
    echo "第一个回调函数注册成功\n";
}

// 尝试注册第二个回调函数(会失败)
if (header_register_callback('second_callback')) {
    echo "第二个回调函数注册成功\n";
} else {
    echo "第二个回调函数注册失败(只能注册一个回调函数)\n";
}

// 输出开始,只有第一个回调函数会被调用
echo "页面内容\n";

// 如果要注册多个回调函数,需要在一个回调函数中调用其他函数
function master_callback() {
    first_callback();
    second_callback();
    third_callback();
}

function third_callback() {
    echo "第三个回调函数\n";
}

// 先清除之前的注册(需要重启请求或使用其他方法)
// 在实际应用中,可以设计一个回调管理器
?>

注意事项

  • 单个回调限制:每个请求只能注册一个回调函数。尝试注册第二个回调函数将失败。
  • 调用时机:回调函数在所有头信息准备好之后,但在任何输出发送之前被调用。
  • 头信息操作:在回调函数中仍然可以使用 header()header_remove() 等函数操作头信息。
  • 输出:回调函数中的输出会先于主脚本的输出发送,这可能影响内容顺序。
  • 错误处理:如果回调函数抛出异常或产生错误,可能会影响整个页面的输出。
  • 性能影响:回调函数会增加请求处理时间,特别是当它执行复杂操作时。
  • 调试:由于回调函数在输出开始前调用,调试可能需要特别的技巧,如记录到日志文件。
  • PHP版本:该函数在 PHP 5.4.0 及更高版本中可用。

常见问题

问题 解决方案
回调函数没有被调用 确保在输出开始前注册回调函数,并且脚本确实有输出。
只能注册一个回调函数 创建一个"主"回调函数,在其中调用多个其他函数。
回调函数中无法设置某些头信息 某些头信息可能被服务器或PHP配置限制,检查头信息是否被允许。
回调函数影响性能 确保回调函数中的操作尽可能高效,避免复杂处理。
如何调试回调函数 将调试信息写入日志文件,而不是直接输出。
回调函数中的错误处理 在回调函数中使用 try-catch 块捕获异常,避免影响主脚本。

实用技巧

1. 创建头部管理器类
<?php
class HeaderCallbackManager {
    private static $callbacks = [];

    public static function add(callable $callback) {
        self::$callbacks[] = $callback;

        // 如果是第一个回调,注册主回调函数
        if (count(self::$callbacks) === 1) {
            header_register_callback([self::class, 'executeAll']);
        }

        return true;
    }

    public static function executeAll() {
        foreach (self::$callbacks as $callback) {
            try {
                call_user_func($callback);
            } catch (Exception $e) {
                error_log("Header callback error: " . $e->getMessage());
            }
        }
    }
}

// 使用示例
HeaderCallbackManager::add(function() {
    header("X-Custom-1: Value1");
});

HeaderCallbackManager::add(function() {
    header("X-Custom-2: Value2");
});
?>
2. 在回调中记录头部信息
<?php
function log_headers_callback() {
    $headers = headers_list();
    $log = "[" . date('Y-m-d H:i:s') . "] Headers to be sent:\n";

    foreach ($headers as $header) {
        $log .= "  " . $header . "\n";
    }

    file_put_contents('/tmp/headers.log', $log, FILE_APPEND);
}

header_register_callback('log_headers_callback');
?>
3. 安全头部自动添加
<?php
function add_security_headers() {
    $security_headers = [
        'X-Frame-Options' => 'DENY',
        'X-Content-Type-Options' => 'nosniff',
        'X-XSS-Protection' => '1; mode=block',
        'Referrer-Policy' => 'strict-origin-when-cross-origin',
        'Permissions-Policy' => 'geolocation=(), microphone=()'
    ];

    foreach ($security_headers as $name => $value) {
        if (!in_array($name, ['Content-Type', 'Location'])) { // 避免覆盖重要头部
            header("$name: $value");
        }
    }
}

header_register_callback('add_security_headers');
?>

相关函数