Khi một website cần thêm tính năng tự viết, câu hỏi đầu tiên không phải “viết code thế nào” mà là “đặt code ở đâu”. Nhét vào theme thì tiện trước mắt, nhưng đổi giao diện là mất sạch. Tách thành một plugin riêng giúp chức năng đó sống độc lập, bật tắt được, và có một vòng đời rõ ràng. Bài này đi từ con số không: header, hook vòng đời, cấu trúc thư mục, và ranh giới giữa plugin với functions.php.
Plugin khác functions.php ở chỗ nào
Cả hai đều là nơi bạn nhét PHP để thay đổi hành vi WordPress, nhưng vai trò khác hẳn. functions.php thuộc về theme (giao diện) — khi bạn đổi sang theme khác, toàn bộ code trong đó ngừng chạy. Plugin thì độc lập: nó nằm trong wp-content/plugins/, không quan tâm bạn dùng giao diện nào.
Quy tắc thực dụng: code nào gắn với cách trang trông (style header, sửa markup template) thì để ở theme; code nào gắn với chức năng phải tồn tại bất kể giao diện (custom post type, đăng ký shortcode, gửi email tự động, tích hợp API) thì phải nằm trong plugin. Một custom post type viết trong functions.php sẽ biến mất khi đổi theme, kéo theo cả nội dung không hiển thị được nữa — đó là lỗi kinh điển của người mới.
Khi Web22 dựng các site khách, mọi logic nghiệp vụ riêng đều được gom vào một plugin tự code (nội bộ gọi là web22-core) chứa custom post type, taxonomy và các chỉnh sửa SEO. Nhờ vậy, đổi giao diện cho khách không bao giờ làm gãy dữ liệu. Nếu bạn muốn hiểu sâu mặt còn lại, bài về cách dùng functions.php gọn gàng bù đắp đúng ranh giới này.

File header — thứ duy nhất bắt buộc
WordPress nhận diện một plugin nhờ một khối chú thích (comment header) ở đầu file PHP chính. Theo tài liệu chính thức, chỉ có một trường thực sự bắt buộc là Plugin Name — phần còn lại là khuyến nghị nhưng nên có đủ để plugin trông chuyên nghiệp.
Tạo thư mục wp-content/plugins/vi-du-plugin/ rồi đặt file vi-du-plugin.php với header sau:
<?php
/**
* Plugin Name: Ví dụ Plugin
* Plugin URI: https://web22.dev/vi-du-plugin
* Description: Một plugin mẫu để học vòng đời WordPress.
* Version: 1.0.0
* Requires at least: 6.5
* Requires PHP: 8.0
* Author: Tên bạn
* License: GPL-2.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: vi-du-plugin
* Domain Path: /languages
*/
// Chặn truy cập trực tiếp vào file.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}Hai dòng cuối quan trọng hơn vẻ ngoài: chúng chặn ai đó gõ thẳng URL tới file PHP của plugin để chạy nó ngoài khung WordPress. ABSPATH chỉ tồn tại khi WordPress đã nạp, nên không có nó thì exit ngay.
Một trường mới đáng chú ý là Requires Plugins (xuất hiện từ WordPress 6.5, tháng 3/2024). Nếu plugin của bạn phụ thuộc một plugin khác trên kho WordPress.org, khai báo slug của nó vào đây, WordPress sẽ tự lo việc không cho kích hoạt khi thiếu — bạn khỏi phải viết logic kiểm tra thủ công.
Ba hook vòng đời activation, deactivation, uninstall
Một plugin tốt phân biệt rõ ba thời điểm: lúc bật, lúc tắt, và lúc bị xóa hẳn. Mỗi thời điểm có một hook riêng và một nguyên tắc xử lý dữ liệu khác nhau.
Activation — chuẩn bị mọi thứ
register_activation_hook() chạy đúng một lần khi người dùng bấm “Kích hoạt”. Đây là nơi tạo bảng cơ sở dữ liệu (database table), ghi tùy chọn mặc định, hoặc nạp lại đường dẫn (flush rewrite rules) nếu plugin có đăng ký custom post type.
function vi_du_kich_hoat() {
// Ghi tùy chọn mặc định nếu chưa có.
add_option( 'vi_du_phien_ban', '1.0.0' );
// Nạp lại đường dẫn cho custom post type/taxonomy mới.
flush_rewrite_rules();
}
register_activation_hook( __FILE__, 'vi_du_kich_hoat' );Tham số __FILE__ chỉ tới chính file plugin hiện tại — đừng thay bằng đường dẫn cứng.
Deactivation — dọn tạm, giữ dữ liệu
register_deactivation_hook() chạy khi người dùng tắt plugin. Nguyên tắc vàng: không xóa dữ liệu thật ở bước này. Người dùng thường tắt plugin chỉ để thử xem nó có xung đột với giao diện hay plugin khác không; nếu bạn xóa cấu hình của họ, bật lại là họ phải nhập từ đầu. Chỉ dọn thứ tạm thời như cache hoặc cron đã lên lịch.
function vi_du_huy_kich_hoat() {
wp_clear_scheduled_hook( 'vi_du_cong_viec_dinh_ky' );
flush_rewrite_rules();
}
register_deactivation_hook( __FILE__, 'vi_du_huy_kich_hoat' );Callback của hook này chạy khi plugin vẫn còn active, nên đây là cơ hội cuối để dùng các hàm thuộc plugin trước khi nó ngừng.
Uninstall — xóa sạch dấu vết
Khi người dùng bấm “Xóa” plugin khỏi danh sách, mới đến lúc dọn vĩnh viễn: xóa option, drop bảng tự tạo. WordPress có hai cách, và cách được khuyến nghị là tạo một file riêng tên uninstall.php ngay trong thư mục gốc plugin.
<?php
// uninstall.php
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
delete_option( 'vi_du_phien_ban' );
// Xóa bảng tự tạo nếu có:
// global $wpdb;
// $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}vi_du_bang" );Vì sao ưu tiên uninstall.php hơn hàm register_uninstall_hook()? Tài liệu chính thức nêu hai lý do. Thứ nhất, register_uninstall_hook() ghi một option mỗi lần được gọi, nên nếu đặt sai chỗ (chạy trên mọi lần tải trang) nó âm thầm bòn rút hiệu năng. Thứ hai, file uninstall.php chạy biệt lập, không nạp toàn bộ file plugin chính — an toàn hơn khi plugin có code chạy ở phạm vi toàn cục. Luôn kiểm tra hằng WP_UNINSTALL_PLUGIN ở đầu file để chặn gọi trực tiếp.
Cấu trúc thư mục khi plugin lớn dần
File đơn lẻ ổn cho plugin nhỏ. Khi chức năng nhiều lên, tách ra để dễ bảo trì. Một bố cục phổ biến:
vi-du-plugin/
├── vi-du-plugin.php (file chính: header + nạp các phần)
├── uninstall.php (dọn dữ liệu khi xóa)
├── readme.txt (mô tả cho kho plugin)
├── inc/ (logic PHP: class, hàm)
│ ├── class-cpt.php
│ └── functions.php
├── admin/ (giao diện trang quản trị)
├── assets/ (css, js, ảnh)
└── languages/ (file dịch .pot/.po/.mo)File chính chỉ nên làm hai việc: khai báo header và require các phần trong inc/. Dùng hằng đường dẫn để gọi file cho gọn và chống lỗi:
define( 'VI_DU_DIR', plugin_dir_path( __FILE__ ) );
require_once VI_DU_DIR . 'inc/class-cpt.php';Bố cục này cũng là khởi điểm để tiến tới một bộ khung tái dùng được. Nếu bạn định viết nhiều plugin, hãy chuẩn hóa thành mẫu sẵn theo bài dựng plugin boilerplate chuẩn 2026 để khỏi gõ lại phần xương sống mỗi lần.

Khi nào viết plugin, khi nào dừng ở functions.php
Không phải chỉnh sửa nào cũng cần một plugin. Bảng dưới phân loại nhanh:
| Tình huống | Nên đặt ở đâu |
|---|---|
| Custom post type, taxonomy | Plugin (phải sống qua đổi theme) |
| Shortcode, tích hợp API, gửi mail tự động | Plugin |
| Đăng ký menu, sửa style giao diện | functions.php của theme |
| Một dòng tinh chỉnh nhỏ chỉ cho site này | functions.php (hoặc child theme) |
| Code muốn tái dùng cho nhiều site | Plugin |
Phép thử đơn giản: tự hỏi “nếu đổi giao diện, tính năng này có được phép biến mất không?”. Nếu câu trả lời là không, nó thuộc về plugin. Việc tách đúng custom post type ra plugin được nói kỹ hơn trong bài tự code custom post type. Nếu bạn không tự code mà muốn thuê làm, dịch vụ thiết kế website của Web22 dựng sẵn site tách bạch theme và plugin chức năng ngay từ đầu.
Web22 thường gói toàn bộ nghiệp vụ riêng của một website vào plugin core tự viết, để sau này nâng cấp hay đổi giao diện cho khách đều an toàn. Nếu bạn cần một website WordPress được dựng tách bạch theme và chức năng ngay từ gốc, tham khảo cách Web22 làm WordPress theo lối lập trình hoặc dịch vụ viết plugin theo yêu cầu.
Câu hỏi thường gặp
Plugin tối thiểu cần bao nhiêu file?
Một file PHP duy nhất với khối header chứa dòng Plugin Name là đủ để WordPress nhận diện và kích hoạt. Mọi file khác chỉ cần khi plugin lớn dần.
Quên flush_rewrite_rules thì sao?
Nếu plugin đăng ký custom post type mà không nạp lại đường dẫn lúc kích hoạt, các trang của post type đó thường báo lỗi 404 cho đến khi bạn vào lại trang Cài đặt đường dẫn tĩnh và lưu.
Có nên xóa dữ liệu lúc deactivation không?
Không. Chỉ dọn dữ liệu tạm (cache, cron) khi tắt; việc xóa option và bảng để dành cho lúc gỡ hẳn qua uninstall.php, tránh làm mất cấu hình của người dùng khi họ chỉ tắt để thử nghiệm.
