不改主题文件做定制:mu-plugin 的四种手法与边界-第三维度MokIII

起因

我想改站点页脚那行版权文字,但打开主题的 footer.php 一看:那句话是硬编码在 PHP 里的——echo 'Copyright © ... . All rights reserved. ...'。主题设置面板里改不到。

摆在面前的只有两条路:改主题文件,或者用 mu-plugin 在输出层动手。我选了后者,这也是我在这个站上一直坚持的规矩:不改主题文件,一律用 mu-plugin。

一、为什么不要直接改主题文件

三个理由,每个都足够充分:

  1. 主题升级会覆盖。改完没记笔记,下次点更新就没了,而且不会有人提醒你;
  2. 出问题难定位。改过的主题和原始主题不一致,报错时你得先花时间确认"这行是不是我改的";
  3. 没法干净回滚。没有版本管理的情况下,你只能凭记忆改回去。

而 mu-plugin 的优势:它是独立文件,删掉就等于没做过——这就是最干净的回滚。

二、mu-plugin 是什么

把 PHP 文件放进 wp-content/mu-plugins/,WordPress 会自动加载,不需要在后台激活,也不出现在插件列表里。

两个特征要先记住:

  • 只按文件名顺序加载,没有依赖管理,所以命名前缀(比如统一用 mokiii-)很重要;
  • 没有"启用/停用",只有"存在/删除"——所以每个文件都要在头部写清楚:它解决什么问题、前提是什么、怎么回滚。

三、四种最常用的手法

1. 改主题读的 option(用于修正主题的数据)

主题读 get_option('theme_option'),你可以挂过滤器改它读到的内容:

add_filter( 'option_theme_option', function ( $value ) {
    if ( is_admin() ) { return $value; }            // 后台保持原样
    $value['footer_text'] = '';                      // 前台清掉
    return $value;
} );

2. 改 ACF 字段的默认值(主题字段组往往在主题里,改不了)

add_filter( 'acf/load_field/name=some_field', function ( $field ) {
    $field['default_value'] = '新的默认值';
    return $field;
} );

只影响"编辑器打开时的预填值",已保存的数据不受影响,安全。

3. 改写主题的输出(用来改硬编码文案,就是本文开头那个场景)

add_action( 'get_footer', function ( $name ) {
    if ( ! empty( $name ) ) { return; }   // 只接管主 footer.php
    ob_start();
}, 1 );

add_action( 'wp_footer', function () {
    $html = ob_get_clean();
    if ( is_string( $html ) && '' !== $html ) {
        echo str_replace( 'All rights reserved.', '保留所有权利。', $html );
    }
}, 0 );

这个手法要注意两点:匹配串必须精确(主题升级改了字面量就会静默失效,所以要在文件头注明);别用它做重活,输出缓冲有代价。

4. 给新内容补默认 meta

add_action( 'save_post', function ( $post_id, $post ) {
    if ( 'post' !== $post->post_type || 'auto-draft' === $post->post_status ) { return; }
    $v = get_post_meta( $post_id, 'some_meta', true );
    if ( '' === $v ) {                       // 严格比较,别用 empty()
        update_post_meta( $post_id, 'some_meta', '1' );
    }
}, 20, 2 );

四、我踩过的两个坑

坑 1:过滤 get_post_metadata 会导致无限递归

我第一版写的"新文章默认带版权声明",用的是过滤 get_post_metadata(想做到"读不到就当默认值")。结果单篇文章直接 HTTP 500。

原因:WordPress 的 metadata_exists() 内部也会触发 get_post_metadata,两边互相调用,直接套娃。改用 save_post 写入真实 meta 就没这个问题。

结论:不要用读过滤器去"伪造"数据,写在写入时机。

坑 2:严格比较 vs empty()

上面第 4 段的 if ( '' === $v ) 是有意的。因为"关闭"这个选项存的是字符串 "0",而 empty("0") 是 true——用 empty() 会把用户明确选择的"关闭"覆盖成默认值。这种"0 是有意义的值"的场景,必须用 ===。

五、什么时候该写正式插件

mu-plugin 也有边界。以下情况不要用它:

  • 需要在后台有开关、有设置页面的功能;
  • 逻辑量大、要分文件、要写测试的;
  • 会被其它站点复用的通用功能。

这些应该做成正式插件。mu-plugin 的定位是小而确定的兜底与修正——一个文件解决一个问题,删掉就干净。


一句话总结:主题文件是"别人的代码",改了迟早要还。用 mu-plugin 把定制放在主题之外,换来的是可回滚和可解释;代价是你要多写几行注释,以及知道它的边界。