wp-plugins

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

WordPress Plugin & Theme Development

WordPress 插件与主题开发

Consolidated reference for plugin architecture, lifecycle, settings, data storage, WP-CLI integration, static analysis, coding standards, testing, and deployment.
Resource files (detailed code examples):
  • resources/settings-api.md
    — Full Settings_Page class implementation
  • resources/wp-cli.md
    — WP-CLI custom commands and operations reference
  • resources/static-analysis-testing.md
    — PHPStan, PHPCS, and PHPUnit testing
  • resources/build-deploy.md
    — Build, deploy, and release workflows

这是一份关于插件架构、生命周期、设置、数据存储、WP-CLI集成、静态分析、编码规范、测试及部署的综合参考文档。
资源文件(含详细代码示例):
  • resources/settings-api.md
    — Settings_Page类完整实现
  • resources/wp-cli.md
    — WP-CLI自定义命令与操作参考
  • resources/static-analysis-testing.md
    — PHPStan、PHPCS及PHPUnit测试指南
  • resources/build-deploy.md
    — 构建、部署与发布工作流

1. Plugin Architecture

1. 插件架构

Main Plugin File

主插件文件

Every plugin starts with a single PHP file containing the plugin header comment. WordPress reads this header to register the plugin in the admin UI.
php
<?php
/**
 * Plugin Name: My Awesome Plugin
 * Plugin URI:  https://example.com/my-awesome-plugin
 * Description: A short description of what this plugin does.
 * Version:     1.0.0
 * Author:      Your Name
 * Author URI:  https://example.com
 * License:     GPL-2.0-or-later
 * Text Domain: my-awesome-plugin
 * Domain Path: /languages
 * Requires at least: 6.2
 * Requires PHP: 7.4
 */

// Prevent direct access.
defined( 'ABSPATH' ) || exit;

// Define constants.
define( 'MAP_VERSION', '1.0.0' );
define( 'MAP_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'MAP_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
define( 'MAP_PLUGIN_FILE', __FILE__ );

// Autoloader (Composer PSR-4).
if ( file_exists( MAP_PLUGIN_DIR . 'vendor/autoload.php' ) ) {
    require_once MAP_PLUGIN_DIR . 'vendor/autoload.php';
}

// Lifecycle hooks — must be registered at top level, not inside other hooks.
register_activation_hook( __FILE__, [ 'MyAwesomePlugin\\Activator', 'activate' ] );
register_deactivation_hook( __FILE__, [ 'MyAwesomePlugin\\Deactivator', 'deactivate' ] );

// Bootstrap the plugin on `plugins_loaded`.
add_action( 'plugins_loaded', function () {
    MyAwesomePlugin\Plugin::instance()->init();
} );
每个插件都以一个包含插件头部注释的PHP文件开头。WordPress通过读取该头部信息在后台界面中注册插件。
php
<?php
/**
 * Plugin Name: My Awesome Plugin
 * Plugin URI:  https://example.com/my-awesome-plugin
 * Description: A short description of what this plugin does.
 * Version:     1.0.0
 * Author:      Your Name
 * Author URI:  https://example.com
 * License:     GPL-2.0-or-later
 * Text Domain: my-awesome-plugin
 * Domain Path: /languages
 * Requires at least: 6.2
 * Requires PHP: 7.4
 */

// 禁止直接访问文件
defined( 'ABSPATH' ) || exit;

// 定义常量
define( 'MAP_VERSION', '1.0.0' );
define( 'MAP_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'MAP_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
define( 'MAP_PLUGIN_FILE', __FILE__ );

// 自动加载器(Composer PSR-4)
if ( file_exists( MAP_PLUGIN_DIR . 'vendor/autoload.php' ) ) {
    require_once MAP_PLUGIN_DIR . 'vendor/autoload.php';
}

// 生命周期钩子 — 必须在顶层作用域注册,不能嵌套在其他钩子内
register_activation_hook( __FILE__, [ 'MyAwesomePlugin\\Activator', 'activate' ] );
register_deactivation_hook( __FILE__, [ 'MyAwesomePlugin\\Deactivator', 'deactivate' ] );

// 在`plugins_loaded`钩子中初始化插件
add_action( 'plugins_loaded', function () {
    MyAwesomePlugin\Plugin::instance()->init();
} );

Bootstrap Pattern

引导模式

  • The main file loads, defines constants, requires the autoloader, registers lifecycle hooks, and defers initialization to a
    plugins_loaded
    callback.
  • Avoid heavy side effects at file load time. Load on hooks.
  • Keep admin-only code behind
    is_admin()
    checks or admin-specific hooks.
  • 主文件加载后,定义常量、引入自动加载器、注册生命周期钩子,然后将初始化逻辑延迟到
    plugins_loaded
    回调中执行。
  • 避免在文件加载时产生大量副作用,应通过钩子加载代码。
  • 仅后台使用的代码需放在
    is_admin()
    判断或后台专属钩子中。

Namespaces and Autoloading (PSR-4)

命名空间与自动加载(PSR-4)

Configure in
composer.json
:
json
{
    "autoload": {
        "psr-4": {
            "MyAwesomePlugin\\": "includes/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "MyAwesomePlugin\\Tests\\": "tests/"
        }
    }
}
Run
composer dump-autoload
after changes.
composer.json
中配置:
json
{
    "autoload": {
        "psr-4": {
            "MyAwesomePlugin\\": "includes/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "MyAwesomePlugin\\Tests\\": "tests/"
        }
    }
}
修改后运行
composer dump-autoload
生效。

Folder Structure

目录结构

my-awesome-plugin/
  my-awesome-plugin.php      # Main plugin file with header
  uninstall.php              # Cleanup on uninstall
  composer.json / phpstan.neon / phpcs.xml / .distignore
  includes/                  # Core PHP classes (PSR-4 root)
    Plugin.php / Activator.php / Deactivator.php
    Admin/  Frontend/  CLI/  REST/
  admin/  public/            # View partials
  assets/  templates/  languages/  tests/
my-awesome-plugin/
  my-awesome-plugin.php      # 带头部注释的主插件文件
  uninstall.php              # 卸载时的清理文件
  composer.json / phpstan.neon / phpcs.xml / .distignore
  includes/                  # 核心PHP类(PSR-4根目录)
    Plugin.php / Activator.php / Deactivator.php
    Admin/  Frontend/  CLI/  REST/
  admin/  public/            # 视图模板片段
  assets/  templates/  languages/  tests/

Singleton vs Dependency Injection

单例模式 vs 依赖注入

Use singleton for the root plugin class only. Prefer dependency injection for everything else (testability).
php
namespace MyAwesomePlugin;

class Plugin {
    private static ?Plugin $instance = null;

    public static function instance(): Plugin {
        if ( null === self::$instance ) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    public function init(): void {
        ( new Admin\Settings_Page() )->register();
        ( new Frontend\Assets() )->register();
        if ( defined( 'WP_CLI' ) && WP_CLI ) {
            CLI\Commands::register();
        }
    }
}
仅根插件类使用单例模式,其他类优先使用依赖注入(提升可测试性)。
php
namespace MyAwesomePlugin;

class Plugin {
    private static ?Plugin $instance = null;

    public static function instance(): Plugin {
        if ( null === self::$instance ) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    public function init(): void {
        ( new Admin\Settings_Page() )->register();
        ( new Frontend\Assets() )->register();
        if ( defined( 'WP_CLI' ) && WP_CLI ) {
            CLI\Commands::register();
        }
    }
}

Action / Filter Hook Architecture

动作/过滤器钩子架构

  • Actions execute code at a specific point (
    do_action
    /
    add_action
    ).
  • Filters modify data and return it (
    apply_filters
    /
    add_filter
    ).
  • Always specify the priority (default 10) and accepted args count.
  • Provide your own hooks so other developers can extend your plugin.
php
// Registering hooks in your plugin.
add_action( 'init', [ $this, 'register_post_types' ] );
add_filter( 'the_content', [ $this, 'append_cta' ], 20 );

// Providing extensibility.
$output = apply_filters( 'map_formatted_price', $formatted, $raw_price );
do_action( 'map_after_order_processed', $order_id );

  • 动作(Actions):在特定执行点运行代码(
    do_action
    /
    add_action
    )。
  • 过滤器(Filters):修改数据并返回结果(
    apply_filters
    /
    add_filter
    )。
  • 始终指定优先级(默认10)和接受的参数数量。
  • 提供自定义钩子,方便其他开发者扩展你的插件。
php
// 在插件中注册钩子
add_action( 'init', [ $this, 'register_post_types' ] );
add_filter( 'the_content', [ $this, 'append_cta' ], 20 );

// 提供扩展能力
$output = apply_filters( 'map_formatted_price', $formatted, $raw_price );
do_action( 'map_after_order_processed', $order_id );

2. Lifecycle Hooks

2. 生命周期钩子

Lifecycle hooks must be registered at top-level scope in the main plugin file, not inside other hooks or conditional blocks.
生命周期钩子必须在主插件文件的顶层作用域注册,不能嵌套在其他钩子或条件语句中。

Activation

激活钩子

php
namespace MyAwesomePlugin;

class Activator {
    public static function activate(): void {
        self::create_tables();

        if ( false === get_option( 'map_settings' ) ) {
            update_option( 'map_settings', [
                'enabled' => true, 'api_key' => '', 'max_results' => 10,
            ], false );
        }

        update_option( 'map_db_version', MAP_VERSION, false );

        // Register CPTs first, then flush so rules exist.
        ( new Plugin() )->register_post_types();
        flush_rewrite_rules();
    }

    private static function create_tables(): void {
        global $wpdb;
        $table = $wpdb->prefix . 'map_logs';
        $sql = "CREATE TABLE {$table} (
            id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
            user_id bigint(20) unsigned NOT NULL DEFAULT 0,
            action varchar(100) NOT NULL DEFAULT '',
            data longtext NOT NULL DEFAULT '',
            created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
            PRIMARY KEY (id),
            KEY user_id (user_id)
        ) {$wpdb->get_charset_collate()};";

        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        dbDelta( $sql );
    }
}
php
namespace MyAwesomePlugin;

class Activator {
    public static function activate(): void {
        self::create_tables();

        if ( false === get_option( 'map_settings' ) ) {
            update_option( 'map_settings', [
                'enabled' => true, 'api_key' => '', 'max_results' => 10,
            ], false );
        }

        update_option( 'map_db_version', MAP_VERSION, false );

        // 先注册自定义文章类型,再刷新重写规则
        ( new Plugin() )->register_post_types();
        flush_rewrite_rules();
    }

    private static function create_tables(): void {
        global $wpdb;
        $table = $wpdb->prefix . 'map_logs';
        $sql = "CREATE TABLE {$table} (
            id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
            user_id bigint(20) unsigned NOT NULL DEFAULT 0,
            action varchar(100) NOT NULL DEFAULT '',
            data longtext NOT NULL DEFAULT '',
            created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
            PRIMARY KEY (id),
            KEY user_id (user_id)
        ) {$wpdb->get_charset_collate()};";

        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        dbDelta( $sql );
    }
}

Deactivation

停用钩子

php
namespace MyAwesomePlugin;

class Deactivator {
    public static function deactivate(): void {
        // Clear scheduled cron events.
        wp_clear_scheduled_hook( 'map_daily_cleanup' );
        wp_clear_scheduled_hook( 'map_hourly_sync' );

        // Flush rewrite rules to remove custom rewrites.
        flush_rewrite_rules();
    }
}
php
namespace MyAwesomePlugin;

class Deactivator {
    public static function deactivate(): void {
        // 清除计划任务
        wp_clear_scheduled_hook( 'map_daily_cleanup' );
        wp_clear_scheduled_hook( 'map_hourly_sync' );

        // 刷新重写规则以移除自定义规则
        flush_rewrite_rules();
    }
}

Uninstall

卸载钩子

Create
uninstall.php
in the plugin root (preferred over
register_uninstall_hook
):
php
<?php
// uninstall.php — runs when plugin is deleted via admin UI.
defined( 'WP_UNINSTALL_PLUGIN' ) || exit;

global $wpdb;
delete_option( 'map_settings' );
delete_option( 'map_db_version' );
$wpdb->query( "DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE 'map\_%'" );
$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}map_logs" );
$wpdb->query(
    "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_map_%'
     OR option_name LIKE '_transient_timeout_map_%'"
);
$wpdb->query( "DELETE FROM {$wpdb->usermeta} WHERE meta_key LIKE 'map\_%'" );
Key rule: Never run expensive operations on every request. Activation, deactivation, and uninstall hooks exist precisely so you can perform setup and teardown only when needed.

在插件根目录创建
uninstall.php
(优先于
register_uninstall_hook
):
php
<?php
// uninstall.php — 当通过后台界面删除插件时运行
defined( 'WP_UNINSTALL_PLUGIN' ) || exit;

global $wpdb;
delete_option( 'map_settings' );
delete_option( 'map_db_version' );
$wpdb->query( "DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE 'map\_%'" );
$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}map_logs" );
$wpdb->query(
    "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_map_%'
     OR option_name LIKE '_transient_timeout_map_%'"
);
$wpdb->query( "DELETE FROM {$wpdb->usermeta} WHERE meta_key LIKE 'map\_%'" );
核心规则:绝不要在每次请求时执行耗时操作。激活、停用和卸载钩子的存在正是为了让你仅在需要时执行设置和清理操作。

3. Settings API

3. 设置API

Full Settings_Page class implementation: see
resources/settings-api.md
.
Settings_Page类的完整实现:详见
resources/settings-api.md

Options API

选项API

php
$settings = get_option( 'map_settings', [] );          // Read.
update_option( 'map_settings', $new_values );           // Write.
delete_option( 'map_settings' );                        // Delete.
update_option( 'map_large_cache', $data, false );       // autoload=false for infrequent options.

php
$settings = get_option( 'map_settings', [] );          // 读取
update_option( 'map_settings', $new_values );           // 写入
delete_option( 'map_settings' );                        // 删除
update_option( 'map_large_cache', $data, false );       // 对不常用的选项设置autoload=false

4. Data Storage

4. 数据存储

When to Use What

存储方案选择

StorageUse CaseSize Guidance
Options APISmall config, plugin settings< 1 MB per option
Post metaPer-post data tied to a specific postKeyed per post
User metaPer-user preferences or stateKeyed per user
Custom tablesStructured, queryable, or large datasetsNo practical limit
TransientsCached data with expirationTemporary, auto-expires
存储方式使用场景大小建议
选项API小型配置、插件设置每个选项小于1MB
文章元数据与特定文章绑定的单篇数据按文章关联
用户元数据每个用户的偏好设置或状态按用户关联
自定义表结构化、可查询或大型数据集无实际限制
Transients带过期时间的缓存数据临时存储,自动过期

Custom Tables with dbDelta

用dbDelta创建自定义表

dbDelta()
rules: each field on its own line, two spaces between name and type,
PRIMARY KEY
with two spaces before, use
KEY
not
INDEX
.
php
function map_create_table(): void {
    global $wpdb;
    $sql = "CREATE TABLE {$wpdb->prefix}map_events (
        id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
        name varchar(255) NOT NULL DEFAULT '',
        status varchar(20) NOT NULL DEFAULT 'draft',
        event_date datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
        PRIMARY KEY  (id),
        KEY status (status)
    ) {$wpdb->get_charset_collate()};";

    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    dbDelta( $sql );
}
dbDelta()
规则:每个字段单独一行,字段名与类型之间用两个空格分隔,
PRIMARY KEY
前加两个空格,使用
KEY
而非
INDEX
php
function map_create_table(): void {
    global $wpdb;
    $sql = "CREATE TABLE {$wpdb->prefix}map_events (
        id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
        name varchar(255) NOT NULL DEFAULT '',
        status varchar(20) NOT NULL DEFAULT 'draft',
        event_date datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
        PRIMARY KEY  (id),
        KEY status (status)
    ) {$wpdb->get_charset_collate()};";

    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    dbDelta( $sql );
}

Schema Upgrades

架构升级

Store a version and compare on
plugins_loaded
:
php
add_action( 'plugins_loaded', function () {
    if ( version_compare( get_option( 'map_db_version', '0' ), MAP_VERSION, '<' ) ) {
        map_create_table();  // dbDelta handles ALTER for existing tables.
        update_option( 'map_db_version', MAP_VERSION, false );
    }
} );
存储版本号并在
plugins_loaded
钩子中对比:
php
add_action( 'plugins_loaded', function () {
    if ( version_compare( get_option( 'map_db_version', '0' ), MAP_VERSION, '<' ) ) {
        map_create_table();  // dbDelta会自动处理现有表的ALTER操作
        update_option( 'map_db_version', MAP_VERSION, false );
    }
} );

Transients

Transients缓存

php
$data = get_transient( 'map_api_results' );
if ( false === $data ) {
    $data = map_fetch_from_api();
    set_transient( 'map_api_results', $data, HOUR_IN_SECONDS );
}
php
$data = get_transient( 'map_api_results' );
if ( false === $data ) {
    $data = map_fetch_from_api();
    set_transient( 'map_api_results', $data, HOUR_IN_SECONDS );
}

Safe SQL

安全SQL操作

Always use
$wpdb->prepare()
for user input. Never concatenate variables into SQL.
php
$results = $wpdb->get_results( $wpdb->prepare(
    "SELECT * FROM {$wpdb->prefix}map_events WHERE status = %s AND event_date > %s",
    $status, $date
) );

对于用户输入,始终使用
$wpdb->prepare()
。绝不要将变量直接拼接进SQL语句。
php
$results = $wpdb->get_results( $wpdb->prepare(
    "SELECT * FROM {$wpdb->prefix}map_events WHERE status = %s AND event_date > %s",
    $status, $date
) );

5. WP-CLI, Static Analysis, Testing, Build & Deploy

5. WP-CLI、静态分析、测试、构建与部署

Detailed references in resource files:
  • WP-CLI (custom commands, operations reference):
    resources/wp-cli.md
  • PHPStan, PHPCS, Testing:
    resources/static-analysis-testing.md
  • Build & Deploy (Composer, JS/CSS, SVN, GitHub releases):
    resources/build-deploy.md

详细参考见资源文件:
  • WP-CLI(自定义命令、操作参考):
    resources/wp-cli.md
  • PHPStan、PHPCS、测试
    resources/static-analysis-testing.md
  • 构建与部署(Composer、JS/CSS、SVN、GitHub发布):
    resources/build-deploy.md

6. Security Checklist

6. 安全检查清单

  • Sanitize on input, escape on output. Never trust
    $_POST
    /
    $_GET
    directly.
  • Use
    wp_unslash()
    before sanitizing superglobals. Read explicit keys only.
  • Pair nonce checks with
    current_user_can()
    . Nonces prevent CSRF, not authz.
  • Use
    $wpdb->prepare()
    for all SQL with user input.
  • Escape output:
    esc_html()
    ,
    esc_attr()
    ,
    esc_url()
    ,
    wp_kses_post()
    .

  • 输入时 sanitize(清理),输出时 escape(转义)。绝不要直接信任
    $_POST
    /
    $_GET
    数据。
  • 清理超全局变量前先使用
    wp_unslash()
    。仅读取明确指定的键值。
  • 将nonce检查与
    current_user_can()
    结合使用。Nonce仅能防止CSRF,不能替代权限验证。
  • 所有包含用户输入的SQL操作都使用
    $wpdb->prepare()
  • 输出转义:使用
    esc_html()
    esc_attr()
    esc_url()
    wp_kses_post()

7. WordPress 6.7-6.9 Breaking Changes

7. WordPress 6.7-6.9 破坏性变更

WP 6.7 -- Translation Loading

WP 6.7 —— 翻译加载机制

load_plugin_textdomain()
is auto-called for plugins with a
Text Domain
header. If you call it manually from
init
, it may load before the preferred translation source (language packs). Move manual calls to
after_setup_theme
or remove them entirely if the header is set.
对于带有
Text Domain
头部的插件,
load_plugin_textdomain()
会被自动调用。如果从
init
钩子中手动调用该函数,可能会优先于语言包加载翻译文件。请将手动调用移至
after_setup_theme
钩子,或在已设置头部的情况下直接移除手动调用。

WP 6.8 -- Bcrypt Password Hashing

WP 6.8 —— Bcrypt密码哈希

WordPress 6.8 migrates from phpass MD5 to bcrypt (
password_hash()
). Existing hashes upgrade transparently on next login. Impact on plugins:
  • wp_check_password()
    /
    wp_hash_password()
    still work -- use these, never hash passwords manually.
  • Plugins that store or compare raw
    $hash
    strings from the
    user_pass
    column will break if they assume
    $P$
    prefix.
  • Custom authentication that bypasses
    wp_check_password()
    must handle both
    $P$
    (legacy) and
    $2y$
    (bcrypt) prefixes.
WordPress 6.8 从phpass MD5迁移到bcrypt(
password_hash()
)。现有哈希值会在用户下次登录时自动升级。对插件的影响
  • wp_check_password()
    /
    wp_hash_password()
    仍然可用 —— 请使用这些函数,绝不要手动哈希密码。
  • 直接存储或比较
    user_pass
    列中原始
    $hash
    字符串的插件会失效,因为新哈希值前缀为
    $2y$
    而非旧的
    $P$
  • 绕过
    wp_check_password()
    的自定义认证逻辑必须同时处理
    $P$
    (旧版)和
    $2y$
    (bcrypt)前缀。

WP 6.9 -- Abilities API & WP_Dependencies Deprecation

WP 6.9 —— 权限API与WP_Dependencies废弃

  • WP_Dependencies
    class is deprecated. Use
    wp_enqueue_script()
    /
    wp_enqueue_style()
    -- never instantiate
    WP_Dependencies
    directly.
  • Abilities API (
    register_ability()
    ) replaces ad-hoc
    current_user_can()
    for REST route permissions (see wp-rest-api skill).

  • WP_Dependencies
    类已被废弃。请使用
    wp_enqueue_script()
    /
    wp_enqueue_style()
    —— 绝不要直接实例化
    WP_Dependencies
  • 权限API(
    register_ability()
    )替代了REST路由权限验证中临时的
    current_user_can()
    用法(详见wp-rest-api相关文档)。

8. Common Mistakes

8. 常见错误

MistakeWhy It FailsFix
Lifecycle hooks inside
add_action('init', ...)
Activation/deactivation hooks not detectedRegister at top-level scope in main file
flush_rewrite_rules()
without registering CPTs first
Rules flushed before custom post types existCall CPT registration, then flush
Missing
sanitize_callback
on
register_setting()
Unsanitized data saved to databaseAlways provide sanitize callback
autoload
left as
yes
for large/infrequent options
Every page load fetches unused dataPass
false
as 4th arg to
update_option()
Using
$wpdb->query()
with string concatenation
SQL injection vulnerabilityUse
$wpdb->prepare()
Nonce check without capability checkCSRF prevented but no authorizationAlways pair nonce +
current_user_can()
innerHTML =
in admin JS
XSS vectorUse
textContent
or DOM creation methods
No
defined('ABSPATH')
guard
Direct file access possibleAdd
defined('ABSPATH') || exit;
to every PHP file
Running
dbDelta()
on every request
Slow table introspection on every page loadRun only on activation or version upgrade
Not checking
WP_UNINSTALL_PLUGIN
in uninstall.php
File could be loaded outside uninstall contextCheck constant before running cleanup
PHPStan baseline grows uncheckedNew errors silently added to baselineReview baseline changes in PRs; never baseline new code
Missing
--dry-run
on
wp search-replace
Irreversible changes to production databaseAlways dry-run first, then backup, then run
Forgetting
--url=
in multisite WP-CLI
Command hits wrong siteAlways include
--url=
for site-specific operations
错误失败原因修复方案
生命周期钩子嵌套在
add_action('init', ...)
激活/停用钩子无法被检测到在主文件的顶层作用域注册
未注册自定义文章类型就调用
flush_rewrite_rules()
重写规则在自定义文章类型存在前被刷新先注册自定义文章类型,再刷新规则
register_setting()
中缺少
sanitize_callback
未清理的数据被保存到数据库始终提供清理回调函数
大型/不常用选项的
autoload
设为
yes
每次页面加载都会获取未使用的数据
update_option()
的第4个参数传入
false
使用
$wpdb->query()
拼接字符串
存在SQL注入漏洞使用
$wpdb->prepare()
仅做Nonce检查而未做权限检查防止了CSRF但未做授权验证始终将Nonce检查与
current_user_can()
结合使用
后台JS中使用
innerHTML =
存在XSS漏洞使用
textContent
或DOM创建方法
缺少
defined('ABSPATH')
防护
文件可能被直接访问在每个PHP文件开头添加
defined('ABSPATH') || exit;
每次请求都运行
dbDelta()
每次页面加载都会执行缓慢的表检查仅在激活或版本升级时运行
uninstall.php中未检查
WP_UNINSTALL_PLUGIN
文件可能在非卸载场景下被加载执行清理前先检查该常量
PHPStan基线无限制增长新错误被静默添加到基线在PR中审查基线变更;绝不为新代码添加基线
wp search-replace
时未加
--dry-run
对生产数据库造成不可逆变更始终先执行试运行,备份后再运行
多站点WP-CLI中忘记
--url=
命令执行到错误的站点针对站点的操作始终添加
--url=
参数