Skip to content

插件开发入门

插件开发概述

什么是 WordPress 插件?

插件是扩展 WordPress 核心功能的 PHP 代码包。与主题不同,插件可以在主题切换时保持功能独立。

插件 vs 主题功能

php
<?php
// functions.php - 主题功能(主题激活时有效)
function theme_feature() {
    // 只能跟随主题
}

// 插件中 - 插件功能(插件启用时有效)
function plugin_feature() {
    // 独立于主题
}

创建第一个插件

目录结构

wp-content/plugins/my-first-plugin/
├── my-first-plugin.php      # 主文件
├── uninstall.php            # 卸载清理
├── readme.txt               # WordPress.org 说明
├── languages/               # 翻译文件
├── includes/                # 核心功能
│   └── class-plugin.php
├── admin/                   # 后台文件
│   ├── css/
│   ├── js/
│   └── partials/
├── public/                  # 前台文件
│   ├── css/
│   └── js/
└── assets/                  # 静态资源
    └── images/

主文件结构

php
<?php
/**
 * Plugin Name: 我的第一个插件
 * Plugin URI: https://example.com/my-first-plugin
 * Description: 插件的简短描述
 * Version: 1.0.0
 * Requires at least: 6.0
 * Requires PHP: 7.4
 * Author: 开发者名称
 * Author URI: https://example.com
 * License: GPL v2 or later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain: my-first-plugin
 * Domain Path: /languages
 */

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

// 定义常量
define('MFP_PLUGIN_VERSION', '1.0.0');
define('MFP_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('MFP_PLUGIN_URL', plugin_dir_url(__FILE__));
define('MFP_PLUGIN_BASENAME', plugin_basename(__FILE__));

// 加载类
require_once MFP_PLUGIN_DIR . 'includes/class-plugin.php';

// 初始化插件
function mfp_init() {
    $plugin = new MyFirstPlugin();
    $plugin->init();
}
add_action('plugins_loaded', 'mfp_init');

// 激活钩子
register_activation_hook(__FILE__, 'mfp_activate');

/**
 * 插件激活时执行
 */
function mfp_activate() {
    // 创建选项
    add_option('mfp_version', MFP_PLUGIN_VERSION);
    
    // 创建数据库表(如果需要)
    mfp_create_tables();
    
    // 刷新永久链接
    flush_rewrite_rules();
}

/**
 * 停用钩子
 */
function mfp_deactivate() {
    // 清理工作
    flush_rewrite_rules();
}
register_deactivation_hook(__FILE__, 'mfp_deactivate');

插件类结构

php
<?php
/**
 * 插件主类
 */
class MyFirstPlugin {
    
    /**
     * 初始化插件
     */
    public function init() {
        // 加载文本域
        $this->load_textdomain();
        
        // 注册钩子
        $this->init_hooks();
        
        // 加载脚本和样式
        $this->enqueue_assets();
    }
    
    /**
     * 加载翻译文件
     */
    public function load_textdomain() {
        load_plugin_textdomain(
            'my-first-plugin',
            false,
            dirname(plugin_basename(__FILE__)) . '/languages/'
        );
    }
    
    /**
     * 初始化钩子
     */
    public function init_hooks() {
        // 添加动作钩子
        add_action('init', array($this, 'register_post_type'));
        add_action('add_meta_boxes', array($this, 'add_meta_box'));
        add_action('save_post', array($this, 'save_meta_box'));
        
        // 添加过滤器钩子
        add_filter('the_content', array($this, 'filter_content'));
        add_filter('excerpt_length', array($this, 'custom_excerpt_length'));
        
        // AJAX 钩子
        add_action('wp_ajax_mfp_save_data', array($this, 'ajax_save_data'));
        add_action('wp_ajax_nopriv_mfp_save_data', array($this, 'ajax_save_data'));
    }
    
    /**
     * 加载资源
     */
    public function enqueue_assets() {
        // 后台样式和脚本
        if (is_admin()) {
            wp_enqueue_style(
                'mfp-admin',
                MFP_PLUGIN_URL . 'admin/css/admin.css',
                array(),
                MFP_PLUGIN_VERSION
            );
            wp_enqueue_script(
                'mfp-admin',
                MFP_PLUGIN_URL . 'admin/js/admin.js',
                array('jquery'),
                MFP_PLUGIN_VERSION,
                true
            );
        }
        
        // 前台样式和脚本
        wp_enqueue_style(
            'mfp-frontend',
            MFP_PLUGIN_URL . 'public/css/frontend.css',
            array(),
            MFP_PLUGIN_VERSION
        );
    }
    
    // ... 更多方法
}

安全的插件开发

输入验证与输出转义

php
<?php
/**
 * 输入验证
 */

// 验证用户权限
if (!current_user_can('manage_options')) {
    wp_die('权限不足');
}

// 验证 Nonce
if (!isset($_POST['mfp_nonce']) || 
    !wp_verify_nonce($_POST['mfp_nonce'], 'mfp_save_action')) {
    wp_die('安全验证失败');
}

// 验证数据类型
$title = isset($_POST['title']) ? sanitize_text_field($_POST['title']) : '';
$email = isset($_POST['email']) ? sanitize_email($_POST['email']) : '';
$url = isset($_POST['url']) ? esc_url_raw($_POST['url']) : '';
$content = isset($_POST['content']) ? wp_kses_post($_POST['content']) : '';
$number = isset($_POST['number']) ? absint($_POST['number']) : 0;
$float = isset($_POST['float']) ? (float) $_POST['float'] : 0.0;

/**
 * 输出转义
 */

// 文本转义
echo esc_html($text);
echo esc_html_e('Text', 'plugin-text-domain');  // 带翻译
echo esc_html__($text, 'plugin-text-domain');

// 属性转义
echo esc_attr($class);
echo esc_url($url);
echo esc_url($link, array('http', 'https'));

// HTML 内容转义
echo wp_kses_post($html);
echo wp_kses($html, $allowed_tags);

// JavaScript 转义
echo esc_js($string);
echo esc_html($string);  // 用于 HTML 中的 JS

// 文本域转义
echo esc_textarea($text);

完整的安全处理示例

php
<?php
/**
 * 保存表单数据
 */
public function save_meta_box($post_id) {
    // 1. 检查自动保存
    if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
        return $post_id;
    }
    
    // 2. 检查用户权限
    if (!current_user_can('edit_post', $post_id)) {
        return $post_id;
    }
    
    // 3. 验证 Nonce
    if (!isset($_POST['mfp_meta_nonce']) || 
        !wp_verify_nonce($_POST['mfp_meta_nonce'], 'mfp_save_meta')) {
        return $post_id;
    }
    
    // 4. 检查文章类型
    if ($_POST['post_type'] !== 'portfolio') {
        return $post_id;
    }
    
    // 5. 保存数据
    if (isset($_POST['mfp_client'])) {
        update_post_meta(
            $post_id,
            'mfp_client',
            sanitize_text_field($_POST['mfp_client'])
        );
    }
    
    if (isset($_POST['mfp_url'])) {
        update_post_meta(
            $post_id,
            'mfp_url',
            esc_url_raw($_POST['mfp_url'])
        );
    }
}

Meta Box 开发

添加 Meta Box

php
<?php
/**
 * 添加 Meta Box
 */
public function add_meta_boxes() {
    add_meta_box(
        'mfp_portfolio_details',                    // ID
        '项目详情',                                  // 标题
        array($this, 'render_meta_box'),           // 回调
        'portfolio',                              // 文章类型
        'normal',                                  // 位置 (normal, side, advanced)
        'high'                                     // 优先级
    );
}

/**
 * 渲染 Meta Box
 */
public function render_meta_box($post) {
    // 获取现有值
    $client = get_post_meta($post->ID, 'mfp_client', true);
    $url = get_post_meta($post->ID, 'mfp_url', true);
    $featured = get_post_meta($post->ID, 'mfp_featured', true);
    
    // Nonce 字段
    wp_nonce_field('mfp_save_meta', 'mfp_meta_nonce');
    ?>
    
    <div class="mfp-meta-box">
        <p>
            <label for="mfp_client">客户名称:</label>
            <input type="text" 
                   id="mfp_client" 
                   name="mfp_client" 
                   value="<?php echo esc_attr($client); ?>"
                   class="widefat">
        </p>
        
        <p>
            <label for="mfp_url">项目链接:</label>
            <input type="url" 
                   id="mfp_url" 
                   name="mfp_url" 
                   value="<?php echo esc_url($url); ?>"
                   class="widefat">
        </p>
        
        <p>
            <label for="mfp_featured">
                <input type="checkbox" 
                       id="mfp_featured" 
                       name="mfp_featured" 
                       value="1"
                       <?php checked($featured, '1'); ?>>
                精选项目
            </label>
        </p>
    </div>
    
    <style>
        .mfp-meta-box p { margin: 1em 0; }
        .mfp-meta-box label { display: block; font-weight: bold; margin-bottom: 5px; }
        .mfp-meta-box input[type="text"],
        .mfp-meta-box input[type="url"] { width: 100%; }
    </style>
    <?php
}

短代码开发

创建短代码

php
<?php
/**
 * 注册短代码
 */
public function register_shortcodes() {
    add_shortcode('mfp_button', array($this, 'render_button_shortcode'));
    add_shortcode('mfp_portfolio_grid', array($this, 'render_portfolio_grid'));
}

/**
 * 按钮短代码
 * 使用方法: [mfp_button url="https://..." text="点击我" style="primary"]
 */
public function render_button_shortcode($atts, $content = null) {
    $atts = shortcode_atts(array(
        'url'    => '#',
        'text'   => '按钮',
        'style'  => 'primary',
        'target' => '_self',
    ), $atts, 'mfp_button');
    
    $classes = 'mfp-button mfp-button-' . esc_attr($atts['style']);
    $target = $atts['target'] === '_blank' ? ' target="_blank" rel="noopener"' : '';
    
    return sprintf(
        '<a href="%s" class="%s"%s>%s</a>',
        esc_url($atts['url']),
        esc_attr($classes),
        $target,
        esc_html($atts['text'])
    );
}

/**
 * 作品集网格短代码
 * 使用方法: [mfp_portfolio_grid count="6" category="web-design"]
 */
public function render_portfolio_grid($atts) {
    $atts = shortcode_atts(array(
        'count'    => 6,
        'category' => '',
        'columns'  => 3,
    ), $atts, 'mfp_portfolio_grid');
    
    $args = array(
        'post_type'      => 'portfolio',
        'posts_per_page' => absint($atts['count']),
    );
    
    if (!empty($atts['category'])) {
        $args['tax_query'] = array(
            array(
                'taxonomy' => 'portfolio_category',
                'field'    => 'slug',
                'terms'    => $atts['category'],
            ),
        );
    }
    
    $query = new WP_Query($args);
    
    if (!$query->have_posts()) {
        return '<p>暂无作品</p>';
    }
    
    ob_start();
    ?>
    <div class="mfp-portfolio-grid mfp-cols-<?php echo esc_attr($atts['columns']); ?>">
        <?php while ($query->have_posts()): $query->the_post(); ?>
            <article class="mfp-portfolio-item">
                <?php if (has_post_thumbnail()): ?>
                    <div class="item-image">
                        <a href="<?php the_permalink(); ?>">
                            <?php the_post_thumbnail('medium'); ?>
                        </a>
                    </div>
                <?php endif; ?>
                <h3><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h3>
            </article>
        <?php endwhile; ?>
    </div>
    <?php
    wp_reset_postdata();
    
    return ob_get_clean();
}

Widget 开发

php
<?php
/**
 * 创建 Widget
 */
class MFP_Recent_Posts_Widget extends WP_Widget {
    
    public function __construct() {
        parent::__construct(
            'mfp_recent_posts',
            '最新作品',
            array('description' => '显示最新的作品集文章')
        );
    }
    
    public function widget($args, $instance) {
        echo $args['before_widget'];
        
        if (!empty($instance['title'])) {
            echo $args['before_title'] . apply_filters('widget_title', $instance['title']) . $args['after_title'];
        }
        
        $query = new WP_Query(array(
            'post_type'      => 'portfolio',
            'posts_per_page' => !empty($instance['count']) ? $instance['count'] : 5,
        ));
        
        if ($query->have_posts()): ?>
            <ul class="mfp-recent-posts">
                <?php while ($query->have_posts()): $query->the_post(); ?>
                    <li>
                        <a href="<?php the_permalink(); ?>">
                            <?php if (has_post_thumbnail()): ?>
                                <?php the_post_thumbnail('thumbnail'); ?>
                            <?php endif; ?>
                            <span><?php the_title(); ?></span>
                        </a>
                    </li>
                <?php endwhile; ?>
            </ul>
        <?php endif;
        
        echo $args['after_widget'];
    }
    
    public function form($instance) {
        $title = !empty($instance['title']) ? $instance['title'] : '最新作品';
        $count = !empty($instance['count']) ? $instance['count'] : 5;
        ?>
        <p>
            <label for="<?php echo $this->get_field_id('title'); ?>">标题:</label>
            <input type="text" 
                   id="<?php echo $this->get_field_id('title'); ?>"
                   name="<?php echo $this->get_field_name('title'); ?>"
                   value="<?php echo esc_attr($title); ?>"
                   class="widefat">
        </p>
        <p>
            <label for="<?php echo $this->get_field_id('count'); ?>">显示数量:</label>
            <input type="number"
                   id="<?php echo $this->get_field_id('count'); ?>"
                   name="<?php echo $this->get_field_name('count'); ?>"
                   value="<?php echo esc_attr($count); ?>"
                   class="widefat">
        </p>
        <?php
    }
    
    public function update($new_instance, $old_instance) {
        $instance = array();
        $instance['title'] = !empty($new_instance['title']) ? sanitize_text_field($new_instance['title']) : '';
        $instance['count'] = !empty($new_instance['count']) ? absint($new_instance['count']) : 5;
        return $instance;
    }
}

// 注册 Widget
function mfp_register_widgets() {
    register_widget('MFP_Recent_Posts_Widget');
}
add_action('widgets_init', 'mfp_register_widgets');

卸载处理

uninstall.php

php
<?php
/**
 * 卸载清理
 */

// 如果不是从 WordPress 调用,则退出
if (!defined('WP_UNINSTALL_PLUGIN')) {
    exit;
}

// 删除插件选项
delete_option('mfp_version');
delete_option('mfp_settings');

// 删除所有文章 meta
$posts = get_posts(array(
    'post_type' => 'portfolio',
    'posts_per_page' => -1,
    'fields' => 'ids',
));

foreach ($posts as $post_id) {
    delete_post_meta($post_id, 'mfp_client');
    delete_post_meta($post_id, 'mfp_url');
    delete_post_meta($post_id, 'mfp_featured');
}

// 删除自定义数据库表(如果有)
global $wpdb;
$wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}mfp_logs");

提交到 WordPress.org

readme.txt 格式

txt
=== 插件名称 ===
Contributors: username
Tags: tag1, tag2
Requires at least: 6.0
Tested up to: 6.4
Stable tag: 1.0.0
Requires PHP: 7.4
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

简短描述(最多 150 字符)

== Description ==

详细描述...

== Installation ==

1. 上传插件到 `/wp-content/plugins/` 目录
2. 通过 WordPress 的"插件"菜单激活
3. 在设置页面配置

== Frequently Asked Questions ==

= 问题 1? =

答案 1。

== Screenshots ==

1. 描述1
2. 描述2

== Changelog ==

= 1.0.0 =
* 首次发布

== Upgrade Notice ==

= 1.0.0 =
首次发布

基于 WordPress官方文档 构建