搜索全站
主题 14 分钟阅读

子主题与父主题 functions.php 执行顺序:函数重写与 pluggable 函数

子主题的 functions.php 是先执行还是后执行?为什么重写父主题函数会报 Cannot redeclare?本文讲清加载顺序、pluggable 函数的覆盖窗口、function_exists 写法与钩子优先级的正确用法。

U
UFO_PF 2016-10-27 · 阅读 14 分钟
子主 主题
阅读提示
本文约 5,401 字,预计阅读 14 分钟。文中命令与配置均经过实测,可直接参考执行。

引言:改了一个函数,整站白屏

子主题里最常见的致命错误长这样:Fatal error: Cannot redeclare astra_setup() (previously declared in .../astra/functions.php:12)。你只是想把父主题的某个函数改一改,把整个函数复制进子主题 functions.php 改了几行——结果整站 500。

同一个错误还有另一种形态:你在子主题里定义了一个同名函数,却完全没有生效,页面行为还是父主题那套。前者是「重复声明」,后者是「声明得晚但父主题已经包了 function_exists 守卫、或者逻辑根本不走函数而走钩子」。要搞清楚这两种相反的结局,就得先弄明白 WordPress 加载主题文件的顺序,以及它和 pluggable.php 的关系。

原理分析:主题加载的完整时间线

WordPress 启动到渲染,与主题相关的顺序大致是:

  1. 加载 wp-config.php、核心文件、wp-settings.php;
  2. 加载启用的插件(active_plugins);
  3. 加载 wp-includes/pluggable.php(定义可被重写的「pluggable 函数」);
  4. 加载子主题的 functions.php;
  5. 加载父主题的 functions.php;
  6. 触发 after_setup_theme;
  7. 后续 init、wp_loaded、wp、模板渲染……

两个关键结论:

结论一:子主题的 functions.php 先于父主题执行。 这是很多人直觉相反的地方。它的意义在于:子主题可以在父主题加载之前,先注册钩子、先 remove_theme_support、先定义常量——但不能指望在这里 remove 掉父主题还没 add 的动作(那是另一个话题,见 子主题修改父主题钩子)。

结论二:pluggable.php 在主题之前加载,且其中的函数用 function_exists() 守卫。 所以插件和主题都可以重新声明这些函数,一旦声明,核心就跳过自己的版本。可重写的函数集中在 wp-includes/pluggable.php,包括 wp_mail、wp_new_user_notification、wp_password_change_notification、wp_notify_postauthor、wp_notify_moderator、get_avatar、wp_redirect(不在 pluggable)、wp_get_current_user、wp_set_current_user、wp_salt(部分版本)、wp_hash 等。准确列表以你所跑版本的 pluggable.php 为准。

父主题自己的函数能不能被覆盖,取决于父主题作者有没有加守卫:

// 父主题里这样写,子主题才能覆盖
if ( ! function_exists( 'astra_setup' ) ) {
    function astra_setup() { /* ... */ }
}

// 父主题里直接声明,子主题再声明就 fatal
function astra_setup() { /* ... */ }

第二种情况,子主题不能用同名函数覆盖,只能换思路:用钩子改行为,或者让父主题作者补上守卫。

保姆级实操

第一步:确认执行顺序(自己验证一遍最靠谱)

// 子主题 functions.php
error_log( 'CHILD functions.php loaded' );

add_action( 'after_setup_theme', function() {
    error_log( 'CHILD after_setup_theme priority 10' );
}, 10 );
// 父主题 functions.php(临时加,验证后删除)
error_log( 'PARENT functions.php loaded' );

打开 WP_DEBUG_LOG,刷新页面看 wp-content/debug.log:

tail -f wp-content/debug.log

你会看到 CHILD 在 PARENT 之前。

第二步:覆盖 pluggable 函数

以改发信行为为例(把系统邮件统一走 SMTP 或加日志):

// 子主题 functions.php —— 无需任何守卫,直接声明
if ( ! function_exists( 'wp_mail' ) ) {
    function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() ) {
        // 1) 记录日志
        error_log( sprintf( 'mail to=%s subject=%s', $to, $subject ) );

        // 2) 交给 SMTP 插件或自己的实现
        if ( class_exists( 'PHPMailer\PHPMailer\PHPMailer' ) ) {
            // ...自定义实现
        }

        // 3) 或调用核心版本(需要先把原函数改名保留)
        return true;
    }
}

实际项目中更推荐用 wp_mail 的过滤器(wp_mail_from、wp_mail_content_type、wp_mail_failed、phpmailer_init)而不是整个重写——重写意味着你要自己处理附件、HTML、编码等全部细节。

第三步:覆盖父主题函数(父主题有守卫时)

// 子主题 functions.php
function astra_setup() {
    load_theme_textdomain( 'astra-child', get_stylesheet_directory() . '/languages' );

    add_theme_support( 'title-tag' );
    add_theme_support( 'post-thumbnails' );
    add_theme_support( 'html5', array( 'search-form', 'comment-form', 'comment-list', 'gallery', 'caption' ) );

    register_nav_menus( array(
        'primary' => __( '主导航', 'astra-child' ),
    ) );
}

因为子主题先加载,这个函数先被声明;父主题加载时看到 function_exists( 'astra_setup' ) 为真,就跳过自己的定义。注意:这种情况下父主题里挂这个函数的 add_action( 'after_setup_theme', 'astra_setup' ) 依然会执行,执行的就是你这份。

第四步:父主题没加守卫时的替代方案

不要硬碰硬,改用钩子。假设父主题这样写:

// 父主题:不可覆盖
function astra_output_footer_credit() {
    echo '<div class="credit">Powered by Astra</div>';
}
add_action( 'wp_footer', 'astra_output_footer_credit', 10 );

子主题可以「覆盖输出」——先移除钩子、再挂自己的:

add_action( 'wp_footer', 'astra_child_replace_credit', 5 );

function astra_child_replace_credit() {
    remove_action( 'wp_footer', 'astra_output_footer_credit', 10 );
    add_action( 'wp_footer', 'astra_child_credit', 10 );
}

function astra_child_credit() {
    echo '<div class="credit">© 2026 迷忙飞</div>';
}

这里把移除动作挂在 wp_footer 的优先级 5(早于 10),此时父主题的回调已经注册(因为 functions.php 都已加载完),移除才有效。

第五步:用类与条件判断提升健壮性

自己写父主题时,推荐的写法是:

if ( ! function_exists( 'mimangfei_setup' ) ) :
    function mimangfei_setup() {
        // ...
    }
endif;
add_action( 'after_setup_theme', 'mimangfei_setup' );

if ( ! class_exists( 'Mimangfei_Walker_Nav_Menu' ) ) {
    class Mimangfei_Walker_Nav_Menu extends Walker_Nav_Menu {
        // 子主题可以定义同名类来替换
    }
}

类同样可以用 class_exists() 守卫,子主题只要先声明同名类,父主题就会跳过——这是分发型主题(如 Genesis 系)常用的扩展方式。

第六步:优先级与钩子的配合

// 子主题想在父主题之后执行 setup
add_action( 'after_setup_theme', 'astra_child_setup', 11 ); // 父主题用 10,这里用 11

function astra_child_setup() {
    remove_theme_support( 'custom-background' );
    add_image_size( 'mf-card', 600, 400, true );
}

remove_theme_support() 必须晚于父主题的 add_theme_support() 才有效,所以优先级要比父主题大。

常见踩坑

  1. 直接复制父主题函数到子主题:父主题没守卫就是 Cannot redeclare 致命错误,整站白屏。
  2. 覆盖了函数但行为没变:父主题的逻辑其实挂在钩子上,或者用了类方法,改函数无效;要顺着 add_action 找真正的回调。
  3. 在 functions.php 顶层就调用 remove_action:子主题先加载,此时父主题还没 add_action,移除无效。必须晚一个优先级。
  4. 重写 wp_mail 后收不到任何邮件:自己实现时漏了附件处理或返回值,插件(如 WP Mail SMTP)也可能已经在 phpmailer_init 上做了接管,两者冲突。
  5. 把 require 的文件也当模板覆盖:require get_template_directory() . '/inc/x.php' 不会被子主题的同名文件替换,父主题要用 get_theme_file_path()。
  6. 案例:某站在子主题里定义了 astra_entry_meta(),前台却仍显示父主题的元信息,且日志里没有报错。原因是父主题该函数带 function_exists 守卫,子主题版本确实生效了——但模板里实际调用的并不是这个函数,而是通过 astra_entry_meta_before 钩子输出的另一段 HTML。排查方式是全局搜索函数名在哪些文件里被调用,而不是假设「同名即生效」。
  7. 优先级写小了:after_setup_theme 用 9 去 remove_theme_support,父主题的 10 还没执行,等于白做。
  8. 子主题里 exit 或 return 提前终止:会打断整个加载流程,导致父主题与后续插件完全不加载。

总结

记住这条时间线:插件 → pluggable → 子主题 functions.php → 父主题 functions.php → after_setup_theme。据此可以推出三条规则:重写 pluggable 函数直接声明即可;覆盖父主题函数的前提是父主题用 function_exists() 守卫(没有就改用钩子);任何「撤销父主题动作」的代码都要晚一个优先级执行。写父主题时请主动给所有函数与类加上守卫,这是让子主题能扩展你的前提。具体的移除时机与优先级细节见 子主题修改父主题钩子,模板层面的覆盖规则见 子主题覆盖模板文件规则。

U
UFO_PF作者

专注 WordPress 生态的实战作者,内容以「可复制、可验证」为第一原则。

留下你的想法

邮箱地址不会被公开。带 * 的为必填项。