搜索全站
报错排查 13 分钟阅读

WordPress functions.php 正确用法:为什么要用功能插件替代

把自定义代码全写进主题的 functions.php,换主题就全丢。本文讲清 functions.php 的加载时机与能力边界、哪些代码该放主题哪些该放插件、如何做一个自己的"功能插件",以及常见的致命写法错误。

U
UFO_PF 2018-10-16 · 阅读 13 分钟
Wo 报错排查
阅读提示
本文约 4,874 字,预计阅读 13 分钟。文中命令与配置均经过实测,可直接参考执行。

引言:换了个主题,站点功能全没了

真实事故:某站在主题 A 的 functions.php 里积累了三年的自定义代码——自定义文章类型、短代码、SEO 逻辑、业务接口。某天换了主题 B,所有功能瞬间消失:CPT 后台菜单没了、文章里的短代码显示成原文、接口 404。

而数据其实都还在数据库里(CPT 的文章记录没丢),只是”定义代码”没了,WordPress 认不出它们。

这类事故的根源是把 functions.php 当成了”万能代码收纳箱”。

原理分析:functions.php 是什么,什么时候加载

functions.php 是主题的”函数文件”,在 WordPress 加载流程里的位置是:

wp-config.php → 核心加载 → 插件加载 → 主题 functions.php → init 钩子 → 渲染

关键特性:

  1. 它属于主题。换主题 = 换一个 functions.php,旧代码全部停用。
  2. 它在插件之后加载。所以 functions.php 里能覆盖插件的钩子,但插件不能覆盖它。
  3. 子主题的 functions.php 先于父主题加载,且父子主题的 functions.php 不会互相覆盖(不像 style.css 或模板文件),两个都会执行。
  4. 每次页面请求都执行(前台和后台都是),所以里面不应该放重量级逻辑。
  5. 它不能定义只在特定页面才需要的复杂流程——那些应该在模板文件或插件里。

能力边界:functions.php 适合放”和这个主题的外观、展示相关”的代码;不适合放”和站点业务功能相关”的代码。

判断标准很简单:如果换主题后你还想保留这段代码,它就不该写在 functions.php 里。

保姆级实操

一、哪些该放 functions.php,哪些该放插件

代码类型 放哪里 理由
注册菜单、侧边栏、主题支持 主题 functions.php 换主题后由新主题自己定义
文章字数统计、阅读时间 主题 functions.php(或功能插件) 展示相关
自定义文章类型(产品、案例) 功能插件 换主题必须保留
短代码 功能插件 换主题必须保留
自定义 REST 端点 功能插件 业务接口
第三方 API 对接 功能插件 业务功能
自定义登录页样式 视情况(客户品牌相关放主题) —
站点级别的 SEO 逻辑 功能插件 与主题无关

二、做一个自己的功能插件(Site Plugin)

这是 WordPress 社区公认的最佳实践。做法非常简单——一个 PHP 文件就够了。

2.1 创建文件

wp-content/plugins/mmf-site/mmf-site.php:

<?php
/**
 * Plugin Name: 迷忙飞站点功能
 * Plugin URI:  https://mimangfei.com
 * Description: 本站自定义功能:文章类型、短代码、接口、业务钩子。与主题无关,换主题不受影响。
 * Version:     1.0.0
 * Author:      mimangfei
 * License:     GPL-2.0+
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;   // 禁止直接访问
}

define( 'MMF_SITE_VERSION', '1.0.0' );
define( 'MMF_SITE_DIR', plugin_dir_path( __FILE__ ) );
define( 'MMF_SITE_URL', plugin_dir_url( __FILE__ ) );

// 按功能拆文件,别堆在一个文件里
require_once MMF_SITE_DIR . 'inc/post-types.php';
require_once MMF_SITE_DIR . 'inc/shortcodes.php';
require_once MMF_SITE_DIR . 'inc/rest-api.php';
require_once MMF_SITE_DIR . 'inc/hooks.php';

2.2 拆分模块

inc/post-types.php:

<?php
add_action( 'init', 'mmf_register_case_cpt' );
function mmf_register_case_cpt() {
    register_post_type( 'case', array(
        'labels'       => array( 'name' => '客户案例', 'singular_name' => '案例' ),
        'public'       => true,
        'show_in_rest' => true,
        'has_archive'  => true,
        'rewrite'      => array( 'slug' => 'case' ),
        'menu_icon'    => 'dashicons-portfolio',
        'supports'     => array( 'title', 'editor', 'thumbnail' ),
    ) );
}

inc/shortcodes.php:

<?php
add_shortcode( 'mmf_year', function() {
    return date( 'Y' );
} );

2.3 启用

wp plugin activate mmf-site
wp plugin list --field=name --status=active

或在后台插件页点启用。

三、functions.php 的常见致命写法

错误 1:文件末尾有多余的闭合标签或空白行

<?php
// ...代码...
?>
<!-- 这里有个换行 -->

?> 之后的换行会被当成输出发送,导致:

  • header() 已经发送,重定向失效(”Cannot modify header information”);
  • RSS Feed 的 XML 前面多一个空行,解析失败;
  • 后台报 “headers already sent”。

规范:PHP 文件末尾不写 ?>。

错误 2:直接用 $_GET / $_POST 不校验

// 危险
$id = $_GET['id'];
echo get_the_title( $id );

// 正确
$id = isset( $_GET['id'] ) ? absint( $_GET['id'] ) : 0;
if ( $id ) {
    echo esc_html( get_the_title( $id ) );
}

还要加 nonce 校验(涉及写操作时):

if ( ! isset( $_POST['mmf_nonce'] ) || ! wp_verify_nonce( $_POST['mmf_nonce'], 'mmf_action' ) ) {
    wp_die( '非法请求' );
}

错误 3:在顶层写需要 WordPress 已完成加载的逻辑

// 错误:functions.php 加载时 $post 还没准备好
global $post;
if ( $post->ID == 123 ) { ... }

// 正确:挂到钩子上
add_action( 'wp', function() {
    if ( is_single( 123 ) ) { ... }
} );

错误 4:函数重名导致致命错误

// 父主题里已有一个 mmf_setup(),子主题再定义一次 → Fatal error: Cannot redeclare
function mmf_setup() { ... }

解决:所有函数加前缀,或者用 function_exists() 包裹:

if ( ! function_exists( 'mmf_setup' ) ) {
    function mmf_setup() { ... }
}

四、子主题 vs 功能插件:什么时候用哪个

场景 方案
改主题的模板结构(single.php、header.php) 子主题
改主题 CSS 子主题 style.css 或自定义器的”额外 CSS”
覆盖主题的某个函数(可插拔函数) 子主题
加自定义文章类型、短代码、接口 功能插件
加主题不涉及的业务逻辑 功能插件

两者可以并存,而且最佳实践就是并存:子主题管外观,功能插件管功能。

五、代码版本管理建议

把 wp-content/plugins/mmf-site/ 和子主题目录纳入 Git:

cd wp-content/plugins/mmf-site
git init
git add .
git commit -m "init site plugin"

.gitignore 参考:

/node_modules
*.log
.DS_Store

六、临时调试代码的正确位置

调试用的 error_log() 建议只在本地/预发布环境加,生产环境用完立刻删除。或者用条件包裹:

if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
    error_log( 'debug: ' . print_r( $data, true ) );
}

常见踩坑与失败案例

  1. 案例一:换主题后 CPT 后台菜单消失、前台短代码变原文。所有”业务功能”代码必须放功能插件。
  2. 案例二:functions.php 末尾有 ?> 加换行,导致 RSS Feed 报 “XML declaration allowed only at the start of the document”。删除闭合标签解决。
  3. 主题更新覆盖了改动:直接改父主题的 functions.php,更新时全部丢失。必须建子主题(不涉及模板改动的功能代码则应放插件)。
  4. 案例三:在 functions.php 顶层写 wp_redirect(),导致后台都进不去(重定向发生在任何页面)。重定向必须挂在 template_redirect 钩子上并加条件判断。
  5. 功能插件停用后内容”消失”:和 CPT 注册代码一样,停用功能插件会让 CPT 定义消失。所以功能插件一旦上线就不要随意停用,或者把 CPT 注册迁到子主题。
  6. 插件里定义了和主题同名的函数:同样会 Fatal error。所有函数都加 mmf_ 前缀。
  7. 把大量代码堆在一个 5000 行的 functions.php:难以维护。按功能拆成多个 require 文件。

总结

核心判断标准一句话:换主题后还想保留的代码,就不要写在 functions.php 里。主题外观相关(菜单、侧边栏、模板覆盖)放子主题;业务功能相关(CPT、短代码、REST 接口、API 对接、自定义钩子)放自建的”功能插件”——一个带标准插件头的 PHP 文件加几个 require 就够了。写法上三条铁律:PHP 文件末尾不写 ?>;所有输入 absint/sanitize_* 并校验 nonce;所有函数加前缀防止重名。这样换主题、升级、迁移都不会丢东西。

U
UFO_PF作者

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

留下你的想法

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