Loading...
Loading...
Compare original and translation side by side
resources/settings-api.mdresources/wp-cli.mdresources/static-analysis-testing.mdresources/build-deploy.mdresources/settings-api.mdresources/wp-cli.mdresources/static-analysis-testing.mdresources/build-deploy.md<?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
/**
* 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();
} );plugins_loadedis_admin()plugins_loadedis_admin()composer.json{
"autoload": {
"psr-4": {
"MyAwesomePlugin\\": "includes/"
}
},
"autoload-dev": {
"psr-4": {
"MyAwesomePlugin\\Tests\\": "tests/"
}
}
}composer dump-autoloadcomposer.json{
"autoload": {
"psr-4": {
"MyAwesomePlugin\\": "includes/"
}
},
"autoload-dev": {
"psr-4": {
"MyAwesomePlugin\\Tests\\": "tests/"
}
}
}composer dump-autoloadmy-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/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();
}
}
}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();
}
}
}do_actionadd_actionapply_filtersadd_filter// 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 );do_actionadd_actionapply_filtersadd_filter// 在插件中注册钩子
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 );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 );
}
}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 );
}
}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();
}
}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.phpregister_uninstall_hook<?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\_%'" );uninstall.phpregister_uninstall_hook<?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\_%'" );resources/settings-api.mdresources/settings-api.md$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.$settings = get_option( 'map_settings', [] ); // 读取
update_option( 'map_settings', $new_values ); // 写入
delete_option( 'map_settings' ); // 删除
update_option( 'map_large_cache', $data, false ); // 对不常用的选项设置autoload=false| 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 | 带过期时间的缓存数据 | 临时存储,自动过期 |
dbDelta()PRIMARY KEYKEYINDEXfunction 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 KEYKEYINDEXfunction 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 );
}plugins_loadedadd_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_loadedadd_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 );
}
} );$data = get_transient( 'map_api_results' );
if ( false === $data ) {
$data = map_fetch_from_api();
set_transient( 'map_api_results', $data, HOUR_IN_SECONDS );
}$data = get_transient( 'map_api_results' );
if ( false === $data ) {
$data = map_fetch_from_api();
set_transient( 'map_api_results', $data, HOUR_IN_SECONDS );
}$wpdb->prepare()$results = $wpdb->get_results( $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}map_events WHERE status = %s AND event_date > %s",
$status, $date
) );$wpdb->prepare()$results = $wpdb->get_results( $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}map_events WHERE status = %s AND event_date > %s",
$status, $date
) );resources/wp-cli.mdresources/static-analysis-testing.mdresources/build-deploy.mdresources/wp-cli.mdresources/static-analysis-testing.mdresources/build-deploy.md$_POST$_GETwp_unslash()current_user_can()$wpdb->prepare()esc_html()esc_attr()esc_url()wp_kses_post()$_POST$_GETwp_unslash()current_user_can()$wpdb->prepare()esc_html()esc_attr()esc_url()wp_kses_post()load_plugin_textdomain()Text Domaininitafter_setup_themeText Domainload_plugin_textdomain()initafter_setup_themepassword_hash()wp_check_password()wp_hash_password()$hashuser_pass$P$wp_check_password()$P$$2y$password_hash()wp_check_password()wp_hash_password()user_pass$hash$2y$$P$wp_check_password()$P$$2y$WP_Dependencieswp_enqueue_script()wp_enqueue_style()WP_Dependenciesregister_ability()current_user_can()WP_Dependencieswp_enqueue_script()wp_enqueue_style()WP_Dependenciesregister_ability()current_user_can()| 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中忘记 | 命令执行到错误的站点 | 针对站点的操作始终添加 |