#1993·typecho

Typecho 1.3 主题 functions.php 钩子失效与修复

Author: AstrsourceCreated Jul 16, 2026Updated Jul 17, 2026
Labelsbug

描述这个 Bug

在 Typecho 1.3.0 版本中,于主题 functions.php 内通过 Plugin::factory 注册 contentEx / excerptEx 钩子,实现短代码替换功能:

php
\Typecho\Plugin::factory('Widget\Abstract\Contents')->contentEx = ['ContentFilter', 'parseContent'];
\Typecho\Plugin::factory('Widget\Abstract\Contents')->excerptEx  = ['ContentFilter', 'parseContent'];

class ContentFilter
{
    public static function parseContent($content, $widget, $lastResult = null): string
    {
        $html = $content;
        $html = self::handleShortcodes($html);
        return $html;
    }
    private static function handleShortcodes($html)
    {
        $html= preg_replace_callback(
            '/\[btn\s+url="([^"]+)"\](.*?)\[\/btn\]/i',
            function ($matches) {
                $url  = trim($matches[1], '`');
                $text = $matches[2];
                return '<a href="' . $url . '" class="custom-btn">' . $text . '</a>';
            },
            $html
        );
        return $html;
    }
}

注册语句本身能正常执行(通过打印确认 hook 已注册成功),但实际渲染时短代码替换完全不生效。

##核心原因

Typecho1.3.0版本__get()属性引入结果缓存,functions.php 的钩子注册晚于 singleHandle,首次触发时钩子不存在,缓存了未替换的原文,后续读取缓存永远跳过钩子。

##修复

将 functions.php 的加载拆为两步:require_once(注册钩子)提前到 handle 调用之前,themeInit()(使用文章数据)保持在 handle 之后。 修改文件:var/Widget/Archive.php 的 execute() 方法。

php
/** 提前加载皮肤函数,使钩子在 handle 触发 content 链之前注册 */
$functionsFile = $this->themeDir . 'functions.php';
$hasFunctionsFile = (!$this->invokeFromOutside || $this->parameter->type == 404 || $this->parameter->preview)
    && file_exists($functionsFile);
if ($hasFunctionsFile) {
    require_once $functionsFile;
}

if (isset($handles[$this->parameter->type])) {
    $handle = $handles[$this->parameter->type];
    $this->{$handle}($select, $hasPushed);
} else {
    $hasPushed = self::pluginHandle()->call('handle', $this->parameter->type, $this, $select);
}

/** 在 handle 执行完毕后调用 themeInit,确保此时已有文章数据 */
if ($hasFunctionsFile && function_exists('themeInit')) {
    themeInit($this);
}