wp-plugins
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWordPress 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):
- — Full Settings_Page class implementation
resources/settings-api.md - — WP-CLI custom commands and operations reference
resources/wp-cli.md - — PHPStan, PHPCS, and PHPUnit testing
resources/static-analysis-testing.md - — Build, deploy, and release workflows
resources/build-deploy.md
这是一份关于插件架构、生命周期、设置、数据存储、WP-CLI集成、静态分析、编码规范、测试及部署的综合参考文档。
资源文件(含详细代码示例):
- — Settings_Page类完整实现
resources/settings-api.md - — WP-CLI自定义命令与操作参考
resources/wp-cli.md - — PHPStan、PHPCS及PHPUnit测试指南
resources/static-analysis-testing.md - — 构建、部署与发布工作流
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 callback.
plugins_loaded - Avoid heavy side effects at file load time. Load on hooks.
- Keep admin-only code behind checks or admin-specific hooks.
is_admin()
- 主文件加载后,定义常量、引入自动加载器、注册生命周期钩子,然后将初始化逻辑延迟到回调中执行。
plugins_loaded - 避免在文件加载时产生大量副作用,应通过钩子加载代码。
- 仅后台使用的代码需放在判断或后台专属钩子中。
is_admin()
Namespaces and Autoloading (PSR-4)
命名空间与自动加载(PSR-4)
Configure in :
composer.jsonjson
{
"autoload": {
"psr-4": {
"MyAwesomePlugin\\": "includes/"
}
},
"autoload-dev": {
"psr-4": {
"MyAwesomePlugin\\Tests\\": "tests/"
}
}
}Run after changes.
composer dump-autoload在中配置:
composer.jsonjson
{
"autoload": {
"psr-4": {
"MyAwesomePlugin\\": "includes/"
}
},
"autoload-dev": {
"psr-4": {
"MyAwesomePlugin\\Tests\\": "tests/"
}
}
}修改后运行生效。
composer dump-autoloadFolder 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 in the plugin root (preferred over ):
uninstall.phpregister_uninstall_hookphp
<?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.phpregister_uninstall_hookphp
<?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.mdSettings_Page类的完整实现:详见。
resources/settings-api.mdOptions 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=false4. Data Storage
4. 数据存储
When to Use What
存储方案选择
| Storage | Use Case | Size Guidance |
|---|---|---|
| Options API | Small config, plugin settings | < 1 MB per option |
| Post meta | Per-post data tied to a specific post | Keyed per post |
| User meta | Per-user preferences or state | Keyed per user |
| Custom tables | Structured, queryable, or large datasets | No practical limit |
| Transients | Cached data with expiration | Temporary, auto-expires |
| 存储方式 | 使用场景 | 大小建议 |
|---|---|---|
| 选项API | 小型配置、插件设置 | 每个选项小于1MB |
| 文章元数据 | 与特定文章绑定的单篇数据 | 按文章关联 |
| 用户元数据 | 每个用户的偏好设置或状态 | 按用户关联 |
| 自定义表 | 结构化、可查询或大型数据集 | 无实际限制 |
| Transients | 带过期时间的缓存数据 | 临时存储,自动过期 |
Custom Tables with dbDelta
用dbDelta创建自定义表
dbDelta()PRIMARY KEYKEYINDEXphp
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 KEYKEYINDEXphp
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_loadedphp
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_loadedphp
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 for user input. Never concatenate variables into SQL.
$wpdb->prepare()php
$results = $wpdb->get_results( $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}map_events WHERE status = %s AND event_date > %s",
$status, $date
) );对于用户输入,始终使用。绝不要将变量直接拼接进SQL语句。
$wpdb->prepare()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 /
$_POSTdirectly.$_GET - Use before sanitizing superglobals. Read explicit keys only.
wp_unslash() - Pair nonce checks with . Nonces prevent CSRF, not authz.
current_user_can() - Use for all SQL with user input.
$wpdb->prepare() - Escape output: ,
esc_html(),esc_attr(),esc_url().wp_kses_post()
- 输入时 sanitize(清理),输出时 escape(转义)。绝不要直接信任/
$_POST数据。$_GET - 清理超全局变量前先使用。仅读取明确指定的键值。
wp_unslash() - 将nonce检查与结合使用。Nonce仅能防止CSRF,不能替代权限验证。
current_user_can() - 所有包含用户输入的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()Text Domaininitafter_setup_theme对于带有头部的插件,会被自动调用。如果从钩子中手动调用该函数,可能会优先于语言包加载翻译文件。请将手动调用移至钩子,或在已设置头部的情况下直接移除手动调用。
Text Domainload_plugin_textdomain()initafter_setup_themeWP 6.8 -- Bcrypt Password Hashing
WP 6.8 —— Bcrypt密码哈希
WordPress 6.8 migrates from phpass MD5 to bcrypt (). Existing
hashes upgrade transparently on next login. Impact on plugins:
password_hash()- /
wp_check_password()still work -- use these, never hash passwords manually.wp_hash_password() - Plugins that store or compare raw strings from the
$hashcolumn will break if they assumeuser_passprefix.$P$ - Custom authentication that bypasses must handle both
wp_check_password()(legacy) and$P$(bcrypt) prefixes.$2y$
WordPress 6.8 从phpass MD5迁移到bcrypt()。现有哈希值会在用户下次登录时自动升级。对插件的影响:
password_hash()- /
wp_check_password()仍然可用 —— 请使用这些函数,绝不要手动哈希密码。wp_hash_password() - 直接存储或比较列中原始
user_pass字符串的插件会失效,因为新哈希值前缀为$hash而非旧的$2y$。$P$ - 绕过的自定义认证逻辑必须同时处理
wp_check_password()(旧版)和$P$(bcrypt)前缀。$2y$
WP 6.9 -- Abilities API & WP_Dependencies Deprecation
WP 6.9 —— 权限API与WP_Dependencies废弃
- class is deprecated. Use
WP_Dependencies/wp_enqueue_script()-- never instantiatewp_enqueue_style()directly.WP_Dependencies - Abilities API () replaces ad-hoc
register_ability()for REST route permissions (see wp-rest-api skill).current_user_can()
- 类已被废弃。请使用
WP_Dependencies/wp_enqueue_script()—— 绝不要直接实例化wp_enqueue_style()。WP_Dependencies - 权限API()替代了REST路由权限验证中临时的
register_ability()用法(详见wp-rest-api相关文档)。current_user_can()
8. Common Mistakes
8. 常见错误
| Mistake | Why It Fails | Fix |
|---|---|---|
Lifecycle hooks inside | Activation/deactivation hooks not detected | Register at top-level scope in main file |
| Rules flushed before custom post types exist | Call CPT registration, then flush |
Missing | Unsanitized data saved to database | Always provide sanitize callback |
| Every page load fetches unused data | Pass |
Using | SQL injection vulnerability | Use |
| Nonce check without capability check | CSRF prevented but no authorization | Always pair nonce + |
| XSS vector | Use |
No | Direct file access possible | Add |
Running | Slow table introspection on every page load | Run only on activation or version upgrade |
Not checking | File could be loaded outside uninstall context | Check constant before running cleanup |
| PHPStan baseline grows unchecked | New errors silently added to baseline | Review baseline changes in PRs; never baseline new code |
Missing | Irreversible changes to production database | Always dry-run first, then backup, then run |
Forgetting | Command hits wrong site | Always include |
| 错误 | 失败原因 | 修复方案 |
|---|---|---|
生命周期钩子嵌套在 | 激活/停用钩子无法被检测到 | 在主文件的顶层作用域注册 |
未注册自定义文章类型就调用 | 重写规则在自定义文章类型存在前被刷新 | 先注册自定义文章类型,再刷新规则 |
| 未清理的数据被保存到数据库 | 始终提供清理回调函数 |
大型/不常用选项的 | 每次页面加载都会获取未使用的数据 | 在 |
使用 | 存在SQL注入漏洞 | 使用 |
| 仅做Nonce检查而未做权限检查 | 防止了CSRF但未做授权验证 | 始终将Nonce检查与 |
后台JS中使用 | 存在XSS漏洞 | 使用 |
缺少 | 文件可能被直接访问 | 在每个PHP文件开头添加 |
每次请求都运行 | 每次页面加载都会执行缓慢的表检查 | 仅在激活或版本升级时运行 |
uninstall.php中未检查 | 文件可能在非卸载场景下被加载 | 执行清理前先检查该常量 |
| PHPStan基线无限制增长 | 新错误被静默添加到基线 | 在PR中审查基线变更;绝不为新代码添加基线 |
| 对生产数据库造成不可逆变更 | 始终先执行试运行,备份后再运行 |
多站点WP-CLI中忘记 | 命令执行到错误的站点 | 针对站点的操作始终添加 |