90 phút
Plugin Development
Cấu trúc plugin
my-plugin/
├── my-plugin.php # Main file với plugin header
├── readme.txt
├── uninstall.php
├── includes/
│ ├── class-my-plugin.php
│ ├── class-admin.php
│ └── class-shortcodes.php
├── admin/
│ ├── css/
│ ├── js/
│ └── views/
├── public/
│ ├── css/
│ ├── js/
│ └── views/
└── languages/
Plugin header
<?php
/**
* Plugin Name: My Plugin
* Plugin URI: https://example.com/my-plugin
* Description: A custom WordPress plugin
* Version: 1.0.0
* Requires at least: 6.0
* Requires PHP: 7.4
* Author: Your Name
* Author URI: https://example.com
* License: GPL v2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: my-plugin
* Domain Path: /languages
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
// Constants
define('MY_PLUGIN_VERSION', '1.0.0');
define('MY_PLUGIN_FILE', __FILE__);
define('MY_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('MY_PLUGIN_URL', plugin_dir_url(__FILE__));
define('MY_PLUGIN_BASENAME', plugin_basename(__FILE__));
// Autoloader hoặc includes
require_once MY_PLUGIN_DIR . 'includes/class-my-plugin.php';
// Activation/Deactivation hooks
register_activation_hook(__FILE__, ['My_Plugin', 'activate']);
register_deactivation_hook(__FILE__, ['My_Plugin', 'deactivate']);
// Initialize
function my_plugin_init() {
My_Plugin::instance();
}
add_action('plugins_loaded', 'my_plugin_init');
Main Plugin Class
<?php
// includes/class-my-plugin.php
class My_Plugin {
private static $instance = null;
public static function instance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
$this->define_hooks();
$this->load_dependencies();
}
private function define_hooks() {
add_action('init', [$this, 'register_post_types']);
add_action('admin_menu', [$this, 'add_admin_menu']);
add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']);
add_action('wp_enqueue_scripts', [$this, 'enqueue_public_assets']);
add_shortcode('my_plugin', [$this, 'render_shortcode']);
add_action('wp_ajax_my_plugin_action', [$this, 'handle_ajax']);
}
private function load_dependencies() {
require_once MY_PLUGIN_DIR . 'includes/class-admin.php';
require_once MY_PLUGIN_DIR . 'includes/class-shortcodes.php';
}
public function enqueue_admin_assets($hook) {
if (strpos($hook, 'my-plugin') === false) {
return;
}
wp_enqueue_style(
'my-plugin-admin',
MY_PLUGIN_URL . 'admin/css/admin.css',
[],
MY_PLUGIN_VERSION
);
wp_enqueue_script(
'my-plugin-admin',
MY_PLUGIN_URL . 'admin/js/admin.js',
['jquery'],
MY_PLUGIN_VERSION,
true
);
wp_localize_script('my-plugin-admin', 'myPlugin', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('my_plugin_nonce'),
]);
}
public function enqueue_public_assets() {
wp_enqueue_style(
'my-plugin-public',
MY_PLUGIN_URL . 'public/css/public.css',
[],
MY_PLUGIN_VERSION
);
wp_enqueue_script(
'my-plugin-public',
MY_PLUGIN_URL . 'public/js/public.js',
[],
MY_PLUGIN_VERSION,
true
);
}
public function add_admin_menu() {
add_menu_page(
__('My Plugin', 'my-plugin'),
__('My Plugin', 'my-plugin'),
'manage_options',
'my-plugin',
[$this, 'render_admin_page'],
'dashicons-admin-generic',
30
);
add_submenu_page(
'my-plugin',
__('Settings', 'my-plugin'),
__('Settings', 'my-plugin'),
'manage_options',
'my-plugin-settings',
[$this, 'render_settings_page']
);
}
public function render_admin_page() {
if (!current_user_can('manage_options')) {
wp_die(__('Access denied', 'my-plugin'));
}
include MY_PLUGIN_DIR . 'admin/views/dashboard.php';
}
public function render_settings_page() {
include MY_PLUGIN_DIR . 'admin/views/settings.php';
}
public function render_shortcode($atts) {
$atts = shortcode_atts([
'title' => 'Default Title',
'limit' => 5,
], $atts, 'my_plugin');
ob_start();
include MY_PLUGIN_DIR . 'public/views/shortcode.php';
return ob_get_clean();
}
public function handle_ajax() {
check_ajax_referer('my_plugin_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'Unauthorized']);
}
$action = sanitize_text_field($_POST['action_type'] ?? '');
switch ($action) {
case 'save_settings':
update_option('my_plugin_option', sanitize_text_field($_POST['value']));
wp_send_json_success(['message' => 'Saved']);
break;
default:
wp_send_json_error(['message' => 'Invalid action']);
}
}
public static function activate() {
// Create database table
global $wpdb;
$table_name = $wpdb->prefix . 'my_plugin_data';
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE $table_name (
id bigint(20) NOT NULL AUTO_INCREMENT,
user_id bigint(20) NOT NULL,
data longtext NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY user_id (user_id)
) $charset_collate;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta($sql);
// Set default options
add_option('my_plugin_version', MY_PLUGIN_VERSION);
// Flush rewrite rules
flush_rewrite_rules();
}
public static function deactivate() {
flush_rewrite_rules();
// Không xóa data khi deactivate
}
}
Admin Page
<!-- admin/views/dashboard.php -->
<div class="wrap my-plugin-admin">
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
<div class="my-plugin-stats">
<div class="stat-card">
<h3><?php esc_html_e('Total Items', 'my-plugin'); ?></h3>
<p class="stat-value"><?php echo esc_html($total_items ?? 0); ?></p>
</div>
</div>
<form id="my-plugin-form" method="post">
<?php wp_nonce_field('my_plugin_save', 'my_plugin_nonce'); ?>
<table class="form-table">
<tr>
<th scope="row">
<label for="my_plugin_setting"><?php esc_html_e('Setting', 'my-plugin'); ?></label>
</th>
<td>
<input type="text"
id="my_plugin_setting"
name="my_plugin_setting"
value="<?php echo esc_attr(get_option('my_plugin_setting', '')); ?>"
class="regular-text">
</td>
</tr>
</table>
<?php submit_button(); ?>
</form>
<button id="my-plugin-ajax-btn" class="button button-primary">
<?php esc_html_e('Test AJAX', 'my-plugin'); ?>
</button>
</div>
Admin JavaScript
// admin/js/admin.js
jQuery(function ($) {
$('#my-plugin-ajax-btn').on('click', function () {
const $btn = $(this);
$btn.prop('disabled', true).text('Loading...');
$.ajax({
url: myPlugin.ajaxUrl,
type: 'POST',
data: {
action: 'my_plugin_action',
nonce: myPlugin.nonce,
action_type: 'save_settings',
value: 'test value'
},
success: function (response) {
if (response.success) {
alert(response.data.message);
} else {
alert('Error: ' + response.data.message);
}
},
error: function () {
alert('AJAX error');
},
complete: function () {
$btn.prop('disabled', false).text('Test AJAX');
}
});
});
});
Settings API
class My_Plugin_Admin {
public function __construct() {
add_action('admin_init', [$this, 'register_settings']);
}
public function register_settings() {
register_setting('my_plugin_settings', 'my_plugin_options', [
'type' => 'array',
'sanitize_callback' => [$this, 'sanitize_options'],
'default' => [
'enabled' => true,
'text' => '',
'color' => '#0073aa',
],
]);
add_settings_section(
'my_plugin_general',
__('General Settings', 'my-plugin'),
[$this, 'section_callback'],
'my_plugin_settings'
);
add_settings_field(
'enabled',
__('Enable feature', 'my-plugin'),
[$this, 'render_checkbox'],
'my_plugin_settings',
'my_plugin_general',
['label_for' => 'enabled', 'field' => 'enabled']
);
add_settings_field(
'text',
__('Text', 'my-plugin'),
[$this, 'render_text_input'],
'my_plugin_settings',
'my_plugin_general',
['label_for' => 'text', 'field' => 'text']
);
}
public function sanitize_options($input) {
return [
'enabled' => !empty($input['enabled']),
'text' => sanitize_text_field($input['text'] ?? ''),
'color' => sanitize_hex_color($input['color'] ?? '#0073aa'),
];
}
public function section_callback() {
echo '<p>' . esc_html__('Configure the plugin behavior.', 'my-plugin') . '</p>';
}
public function render_checkbox($args) {
$options = get_option('my_plugin_options', []);
$field = $args['field'];
?>
<input type="checkbox"
id="<?php echo esc_attr($field); ?>"
name="my_plugin_options[<?php echo esc_attr($field); ?>]"
value="1"
<?php checked(!empty($options[$field])); ?>>
<?php
}
public function render_text_input($args) {
$options = get_option('my_plugin_options', []);
$field = $args['field'];
?>
<input type="text"
id="<?php echo esc_attr($field); ?>"
name="my_plugin_options[<?php echo esc_attr($field); ?>]"
value="<?php echo esc_attr($options[$field] ?? ''); ?>"
class="regular-text">
<?php
}
}
Custom Database Table
class My_Plugin_DB {
public static function get_table() {
global $wpdb;
return $wpdb->prefix . 'my_plugin_data';
}
public static function create(array $data): int|false {
global $wpdb;
$result = $wpdb->insert(
self::get_table(),
[
'user_id' => $data['user_id'],
'data' => maybe_serialize($data['data']),
'created_at' => current_time('mysql'),
],
['%d', '%s', '%s']
);
return $result ? $wpdb->insert_id : false;
}
public static function get(int $id): ?object {
global $wpdb;
$table = self::get_table();
return $wpdb->get_row(
$wpdb->prepare("SELECT * FROM $table WHERE id = %d", $id)
);
}
public static function get_by_user(int $user_id): array {
global $wpdb;
$table = self::get_table();
return $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM $table WHERE user_id = %d ORDER BY created_at DESC",
$user_id
)
);
}
public static function delete(int $id): bool {
global $wpdb;
return (bool) $wpdb->delete(
self::get_table(),
['id' => $id],
['%d']
);
}
}
Shortcode
class My_Plugin_Shortcodes {
public function __construct() {
add_shortcode('my_plugin', [$this, 'render']);
add_shortcode('my_plugin_form', [$this, 'render_form']);
}
public function render($atts): string {
$atts = shortcode_atts([
'title' => __('My Plugin', 'my-plugin'),
'limit' => 5,
], $atts, 'my_plugin');
$items = My_Plugin_DB::get_by_user(get_current_user_id());
$items = array_slice($items, 0, (int) $atts['limit']);
ob_start();
?>
<div class="my-plugin-shortcode">
<h3><?php echo esc_html($atts['title']); ?></h3>
<?php if ($items): ?>
<ul>
<?php foreach ($items as $item): ?>
<li><?php echo esc_html($item->id); ?> -
<?php echo esc_html($item->created_at); ?></li>
<?php endforeach; ?>
</ul>
<?php else: ?>
<p><?php esc_html_e('No items found.', 'my-plugin'); ?></p>
<?php endif; ?>
</div>
<?php
return ob_get_clean();
}
public function render_form(): string {
ob_start();
?>
<form class="my-plugin-form" method="post">
<?php wp_nonce_field('my_plugin_submit', 'my_plugin_nonce'); ?>
<input type="text" name="my_data" required>
<button type="submit"><?php esc_html_e('Submit', 'my-plugin'); ?></button>
</form>
<?php
return ob_get_clean();
}
}
REST API Endpoints
add_action('rest_api_init', function () {
register_rest_route('my-plugin/v1', '/items', [
[
'methods' => WP_REST_Server::READABLE,
'callback' => 'my_plugin_get_items',
'permission_callback' => function () {
return is_user_logged_in();
},
],
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => 'my_plugin_create_item',
'permission_callback' => function () {
return current_user_can('edit_posts');
},
'args' => [
'title' => [
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => function ($value) {
return strlen($value) >= 2;
},
],
],
],
]);
});
function my_plugin_get_items(WP_REST_Request $request): WP_REST_Response {
$items = My_Plugin_DB::get_by_user(get_current_user_id());
return new WP_REST_Response($items, 200);
}
function my_plugin_create_item(WP_REST_Request $request): WP_REST_Response {
$id = My_Plugin_DB::create([
'user_id' => get_current_user_id(),
'data' => ['title' => $request->get_param('title')],
]);
if (!$id) {
return new WP_REST_Response(['error' => 'Failed to create'], 500);
}
return new WP_REST_Response(['id' => $id], 201);
}
Uninstall
<?php
// uninstall.php
if (!defined('WP_UNINSTALL_PLUGIN')) {
exit;
}
global $wpdb;
// Drop custom tables
$wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}my_plugin_data");
// Delete options
delete_option('my_plugin_version');
delete_option('my_plugin_settings');
delete_option('my_plugin_options');
// Delete user meta
delete_metadata('user', 0, 'my_plugin_user_setting', '', true);
// Delete posts (nếu plugin tạo CPT)
$posts = get_posts([
'post_type' => 'my_cpt',
'numberposts' => -1,
'post_status' => 'any',
]);
foreach ($posts as $post) {
wp_delete_post($post->ID, true);
}
Bài tập thực hành
Hãy tạo plugin đầy đủ với admin page, shortcode và REST API!