KIếN THứC WEBSITE › WORDPRESS

Tạo plugin WordPress từ đầu — 7 bước build chuẩn

Tạo plugin WordPress từ đầu — 7 bước build chuẩn 2026

Plugin WordPress là layer mở rộng độc lập với theme — feature giữ nguyên khi đổi theme, dữ liệu không bị mất. Bài này hướng dẫn 7 bước build plugin từ đầu năm 2026: cấu trúc folder, plugin header 14 field, lifecycle hook, Settings API, WordPress Coding Standards, test trên fresh install, submit Plugin Directory.

Mọi block code đều copy-paste chạy được trên WordPress 6.5 + PHP 8.1.

Plugin WordPress trong context 2026 — bản chất và 3 use case cần biết

tạo plugin wordpress — Plugin WordPress trong context 2026 — bản chất và 3 use case cần biết
Sơ đồ minh hoạ — Plugin WordPress trong context 2026 — bản chất và 3 use case cần biết
Plugin WordPress trong context 2026 — bản chất và 3 use case cần biết
Sơ đồ minh hoạ — Plugin WordPress trong context 2026 — bản chất và 3 use case cần biết

Plugin WordPress là gói code PHP độc lập, cài qua admin Plugins → Upload, kích hoạt hoặc vô hiệu hoá bất kỳ lúc nào mà không ảnh hưởng theme đang dùng.

Khác biệt cốt lõi so với theme: mỗi site chỉ active 1 theme còn plugin có thể chạy song song nhiều bộ. Dữ liệu plugin lưu trong bảng riêng hoặc post meta, tồn tại độc lập kể cả khi đổi theme.

Khi nào nên tách code thành plugin riêng

Code đẩy vào functions.php nhanh và tiện cho prototype nhưng có giới hạn rõ. Ba dấu hiệu nên chuyển sang plugin để tránh nợ kỹ thuật.

  • Custom Post Type cần tồn tại qua mọi theme: CPT đăng ký trong functions.php sẽ biến mất khỏi admin khi switch theme — post data còn trong DB nhưng không edit được nữa.
  • Feature dùng chung nhiều site: form AJAX custom, tích hợp ZaloPay hoặc MoMo, schema markup riêng — đóng thành plugin để reuse thay vì copy code từng project.
  • Code dự định submit Plugin Directory: WordPress.org chỉ chấp nhận structure plugin chuẩn, không submit được từ theme functions.

3 trường hợp KHÔNG cần build plugin riêng

Plugin không phải lựa chọn mặc định cho mọi snippet. Một số bối cảnh dùng functions.php hoặc mu-plugin hợp lý hơn.

  • Snippet 5-10 dòng cho 1 site duy nhất: hook nhỏ vào wp_head hay filter the_content có thể giữ trong child theme functions.php — đỡ tốn overhead activate plugin.
  • Code system-critical phải luôn chạy: security baseline, network-wide config nên dùng mu-plugin trong wp-content/mu-plugins/ — không deactivate được qua admin UI.
  • Logic phụ thuộc chặt vào markup theme: shortcode dùng class CSS riêng của theme nên giữ trong theme — tách ra plugin sẽ vỡ khi đổi theme.

Bước 1 — Cấu trúc folder plugin tối thiểu cho 2026

Bước 1 — Cấu trúc folder plugin tối thiểu cho 2026
Bước 1 — Cấu trúc folder plugin tối thiểu cho 2026

Plugin nhỏ chỉ cần 1 file PHP có header là đã chạy được. Plugin scale từ 5 feature trở lên cần chia folder rõ để onboard dev mới không mất ngày đọc code.

Cấu trúc folder khuyến nghị

w22-forms/
├── w22-forms.php          # Main file, chứa header + bootstrap
├── uninstall.php          # Cleanup khi user delete plugin
├── readme.txt             # Metadata cho Plugin Directory
├── composer.json          # Optional, dùng khi có Composer dependency
├── includes/
│   ├── class-plugin.php
│   ├── class-form-handler.php
│   └── helpers.php
├── admin/
│   ├── class-settings-page.php
│   └── views/
├── public/
│   ├── class-shortcode.php
│   └── assets/
└── languages/
    └── w22-forms.pot

Vai trò từng folder

Mỗi folder mang 1 trách nhiệm cụ thể — tránh nhồi logic admin vào public hoặc ngược lại. Đây là 5 folder cần nắm trước khi viết dòng code đầu tiên.

  • Root + main file: chứa header metadata, define constant, require bootstrap — KHÔNG nhét logic feature vào đây.
  • includes/: core class và helper function chạy ở mọi context — không phụ thuộc admin hay frontend.
  • admin/: settings page, metabox, custom column — chỉ load khi is_admin() trả true.
  • public/: hook frontend, shortcode, REST endpoint public — load ở ngữ cảnh visitor.
  • languages/: file .pot, .po, .mo cho đa ngôn ngữ — path phải khớp Domain Path khai báo trong header.

Plugin header là khối comment PHP đầu file main, WordPress parse khi quét folder plugins. Thiếu 1 field bắt buộc thì plugin không xuất hiện trong admin.

Header đầy đủ 14 field

<?php
/**
 * Plugin Name:       Web22 Custom Forms
 * Plugin URI:        https://web22.dev/plugins/custom-forms
 * Description:       Form AJAX có honeypot, tích hợp HubSpot và ZaloPay.
 * Version:           1.0.0
 * Requires at least: 6.0
 * Requires PHP:      8.0
 * Author:            Web22.dev
 * Author URI:        https://web22.dev
 * License:           GPL-2.0-or-later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       w22-forms
 * Domain Path:       /languages
 * Update URI:        https://web22.dev/api/plugin-updates
 * Requires Plugins:  woocommerce
 */

if (!defined('ABSPATH')) exit;

define('W22_FORMS_VERSION', '1.0.0');
define('W22_FORMS_PATH', plugin_dir_path(__FILE__));
define('W22_FORMS_URL', plugin_dir_url(__FILE__));

require_once W22_FORMS_PATH . 'includes/class-plugin.php';

3 field mới của WordPress 6.5 cần lưu ý

Phiên bản 6.5 bổ sung và làm rõ một số field mà plugin cũ thường thiếu. Biết sớm tránh phải refactor header khi WordPress nâng version major.

  • Update URI: chỉ định endpoint custom cho cơ chế update — bắt buộc nếu plugin host ngoài WordPress.org để tránh xung đột slug.
  • Requires Plugins: liệt kê plugin dependency theo slug — WordPress tự deactivate nếu dependency chưa active, không cần code check thủ công.
  • Requires PHP: WordPress check version trước khi load — PHP thấp hơn yêu cầu sẽ bị chặn activate, không gây white screen.

Bước 3 — Activation, deactivation và uninstall hook

Ba hook lifecycle quyết định plugin có install và uninstall sạch sẽ không. Sai pattern dẫn tới data orphan trong database, vi phạm guideline khi submit Plugin Directory.

register_activation_hook — chạy 1 lần khi activate

register_activation_hook(__FILE__, 'w22_forms_activate');

function w22_forms_activate() {
    global $wpdb;
    $table = $wpdb->prefix . 'w22_forms_submissions';
    $charset = $wpdb->get_charset_collate();
    $sql = "CREATE TABLE IF NOT EXISTS $table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        created_at DATETIME NOT NULL,
        email VARCHAR(190) NOT NULL,
        payload LONGTEXT NOT NULL,
        INDEX idx_email (email)
    ) $charset;";
    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    dbDelta($sql);
    add_option('w22_forms_version', W22_FORMS_VERSION);
    flush_rewrite_rules();
}

register_deactivation_hook — giữ data, dọn cron

register_deactivation_hook(__FILE__, function () {
    wp_clear_scheduled_hook('w22_forms_cron');
    flush_rewrite_rules();
    // KHÔNG xoá table hay option ở đây — user có thể activate lại
});

uninstall.php — file riêng, không cần register

WordPress tự gọi uninstall.php khi user click Delete plugin trong admin. File này đặt cùng cấp với main file, chạy độc lập không cần register hook.

<?php
if (!defined('WP_UNINSTALL_PLUGIN')) exit;

global $wpdb;
$wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}w22_forms_submissions");
delete_option('w22_forms_version');
delete_option('w22_forms_settings');
delete_metadata('post', 0, '_w22_forms_meta', '', true);

Bước 4 — Settings page với Settings API chuẩn

Settings page là feature đầu tiên hầu hết plugin cần có. WordPress cung cấp Settings API sẵn — không phải tự build form, validation, nonce từ đầu.

Đăng ký menu trong admin

add_action('admin_menu', function () {
    add_options_page(
        'Web22 Forms',
        'Web22 Forms',
        'manage_options',
        'w22-forms',
        'w22_forms_render_page'
    );
});

function w22_forms_render_page() {
    echo '<div class="wrap"><h1>Web22 Forms</h1>';
    echo '<form method="post" action="options.php">';
    settings_fields('w22_forms_group');
    do_settings_sections('w22-forms');
    submit_button();
    echo '</form></div>';
}

Register setting kèm sanitize callback

add_action('admin_init', function () {
    register_setting('w22_forms_group', 'w22_forms_email', [
        'type' => 'string',
        'sanitize_callback' => 'sanitize_email',
        'default' => get_option('admin_email'),
    ]);
    add_settings_section('main', 'Cấu hình chính', null, 'w22-forms');
    add_settings_field('email', 'Email nhận form', function () {
        $v = get_option('w22_forms_email', '');
        echo '<input type="email" name="w22_forms_email" value="' . esc_attr($v) . '" class="regular-text">';
    }, 'w22-forms', 'main');
});

3 nguyên tắc bảo mật cho Settings page

Settings page xử lý input từ admin nhưng vẫn cần guard đầy đủ — không phải cứ manage_options là an toàn tuyệt đối. Ba nguyên tắc bắt buộc khi viết form admin.

  • Sanitize input mọi field: dùng sanitize_email, sanitize_text_field, esc_url_raw tuỳ data type — KHÔNG tin tưởng input dù từ admin.
  • Escape mọi output: dùng esc_attr, esc_html, wp_kses_post khi render value ra HTML — chặn XSS reflected từ option bị tamper.
  • Capability check rõ ràng: current_user_can('manage_options') trong callback render và save — Settings API check sẵn nhưng custom flow phải check tay.

Bước 5 — WordPress Coding Standards với PHPCS

WordPress Coding Standards là PHPCS ruleset quy định format code chính thức. Bắt buộc nếu submit Plugin Directory, khuyến nghị cho mọi plugin có 2 dev trở lên cùng làm.

Cài đặt và chạy PHPCS qua Composer

# Cài qua Composer
composer require --dev wp-coding-standards/wpcs

# Run check
vendor/bin/phpcs --standard=WordPress w22-forms.php

# Auto-fix những lỗi có thể fix tự động
vendor/bin/phpcbf --standard=WordPress w22-forms.php

4 rule WPCS hay vi phạm nhất

Đa số report PHPCS rơi vào 4 nhóm rule sau. Nắm trước để code lần đầu đã pass thay vì phải fix hàng loạt sau.

  • Yoda condition: WPCS yêu cầu if ('foo' === $x) thay vì if ($x === 'foo') — tránh nhầm = với == khi gõ vội.
  • Escape mọi output: echo trực tiếp variable không escape sẽ bị report — dùng esc_html, esc_attr, wp_kses_post tương ứng context.
  • Nonce verify mọi form: phải gọi check_admin_referer hoặc wp_verify_nonce trước khi process $_POST — chặn CSRF.
  • Text domain literal: mọi __()_e() phải truyền text domain string literal, không truyền qua variable.

Bước 6 — Test trên fresh install trước khi release

Plugin chạy được trên máy dev không đảm bảo chạy được trên site khách. Test trên fresh WordPress install bắt được 70-80% bug xung đột môi trường.

Setup môi trường test với XAMPP

XAMPP cài 10-15 phút trên Windows hoặc Mac, kèm PHP 8.1 và MySQL 8. Tạo fresh WordPress với wp core downloadwp core install qua WP-CLI nhanh hơn cài tay 5-10 lần.

Checklist 6 mục test trước khi đóng zip

  • Activate không có warning: bật WP_DEBUG = trueWP_DEBUG_LOG = true — kiểm debug.log không có notice hay deprecated nào.
  • Deactivate giữ data: deactivate rồi activate lại — settings và option phải còn nguyên, không reset về default.
  • Uninstall dọn sạch: delete plugin qua admin — check DB không còn table, option hay meta nào của plugin sót lại.
  • Đa ngôn ngữ chuẩn: generate file .pot bằng lệnh wp i18n make-pot . languages/w22-forms.pot — đảm bảo mọi string đều được wrap __().
  • readme.txt validate qua tool chính thức: upload qua readme validator của WordPress.org trước khi submit.
  • Test với 5 plugin phổ biến cài kèm: WooCommerce, Yoast SEO, Wordfence, WP Rocket, Elementor — bắt xung đột hook hoặc enqueue duplicate.

Bước 7 — Submit Plugin Directory và quy trình review

Submit Plugin Directory mở cửa phân phối miễn phí tới hơn 60 triệu site dùng WordPress. Quy trình review khắt khe, fail lần đầu là chuyện bình thường — sửa rồi submit lại không bị phạt.

Workflow submit chuẩn

  1. Zip plugin folder w22-forms/ giữ nguyên cấu trúc — không zip lồng folder trong folder.
  2. Upload qua trang wordpress.org/plugins/developers/add — đăng nhập tài khoản WordPress.org.
  3. Đợi review tự động bằng tool scan code — thường trả kết quả trong 24-48 giờ.
  4. Đợi human review — đội plugin team check guideline thủ công, mất từ 1 đến 14 ngày tuỳ tải.
  5. Nếu pass, được cấp SVN repo riêng — push code lên trunk và tag để release version đầu tiên.
  6. Submit asset (banner, icon, screenshot) qua folder assets/ trên SVN — không cần đợi review riêng.

3 lý do thường bị reject lần đầu

Plugin team WordPress.org có guideline rõ — đọc trước tiết kiệm 1-2 vòng review. Ba lý do reject phổ biến nhất theo thống kê công khai 2025.

  • Không escape output: output variable chưa qua esc_html hoặc esc_attr là lý do reject số 1 — chiếm khoảng 40% case.
  • Load resource external: nhúng JS hoặc CSS từ CDN bên thứ ba (vd Google CDN, jsDelivr) — yêu cầu phải bundle local vào plugin.
  • Trademark vi phạm: tên plugin chứa từ trademark như “WooCommerce” hay “Yoast” mà không có “Add-on for” hoặc tương đương rõ.

Câu hỏi thường gặp

Plugin và mu-plugin khác nhau như thế nào?

mu-plugin (must-use plugin) tự động active, không deactivate được qua admin UI, nằm ở wp-content/mu-plugins/. Dùng cho code system-critical như security baseline hoặc network-wide config trên multisite.

Plugin thường nằm ở wp-content/plugins/ và user toggle active qua admin. Hầu hết feature business nên dùng plugin thường — mu-plugin chỉ cho code hạ tầng không được phép tắt.

Khi nào dùng class còn khi nào procedural function?

Plugin dưới 200 dòng với 1 feature dùng procedural là đủ — function thuần dễ đọc, không over-engineer. Plugin scale lớn hoặc nhiều feature nên dùng class với singleton hoặc DI container.

Tham khảo pattern OOP đầy đủ trong bài Plugin boilerplate WordPress 2026 — WPPB vs Underscores vs custom — phân tích chi tiết khi nào chọn cách tổ chức code nào.

Plugin có bắt buộc dùng Composer không?

Không bắt buộc. Composer chỉ cần khi plugin dùng thư viện ngoài như Stripe SDK hoặc Symfony component.

Plugin pure WordPress hook không cần — require_once thủ công đủ dùng.

Khi submit Plugin Directory cần chú ý: Composer dependency phải được scoped bằng PHP-Scoper để tránh xung đột version với plugin khác cài chung site.

flush_rewrite_rules có tốn performance không?

Có. Chỉ nên gọi trong activation hook hoặc khi user manually trigger qua admin UI — KHÔNG bao giờ gọi trong init mỗi request vì sẽ rewrite option trên mọi page load, tốn 50-200ms tuỳ size DB.

Pattern an toàn: đăng ký CPT và taxonomy trong init như bình thường, chỉ gọi flush_rewrite_rules trong register_activation_hook — chi tiết trong register_post_type WordPress — 25 tham số + show_in_rest 2026.

Tại sao phải check ABSPATH ở đầu mỗi file PHP?

Đây là rào chắn bảo mật. Nếu attacker truy cập trực tiếp file PHP qua URL (vd scan tìm vulnerability), defined('ABSPATH') trả false thì script exit ngay — không expose code hay path nhạy cảm.

Pattern bắt buộc cho mọi file PHP trong plugin, kể cả file include nội bộ. Bỏ qua check này là lý do reject phổ biến khi submit Plugin Directory.

Tài nguyên và bước tiếp theo

Nắm 7 bước trên là đã build được plugin production-grade. Mở rộng sang các topic chuyên sâu để hoàn thiện stack WordPress development theo định hướng 2026.

Cần đội Web22 dev plugin custom theo brief của shop hoặc dự án? Dịch vụ dev theme/plugin WordPress custom tại Web22 — bàn giao kèm test fresh install, readme validate và bảo hành 12 tháng.