Giáo trình ôn tập WordPress bài bản + Vibe Coding
Chương 2 – Plugin Development + Custom Post Type/Taxonomy
PHẦN A: CẤU TRÚC PLUGIN CHUẨN
1. File chính của plugin — Header bắt buộc
<?php
/**
* Plugin Name: My Book Manager
* Plugin URI: https://example.com
* Description: Quản lý sách qua Custom Post Type
* Version: 1.0.0
* Author: Your Name
* License: GPL v2 or later
* Text Domain: my-book-manager
*/
// Chặn truy cập trực tiếp file — thói quen bảo mật bắt buộc
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
Giống theme, WordPress đọc comment header này để nhận diện plugin, hiện tên/mô tả trong danh sách Plugins ở wp-admin. Thiếu Plugin Name → WP không nhận đây là plugin hợp lệ.
2. Vì sao nên tổ chức theo Class thay vì hàm rời rạc
Cách yếu (hay gặp ở code cũ/tự học):
function my_register_cpt() { ... }
add_action('init', 'my_register_cpt');
function my_add_meta_box() { ... }
add_action('add_meta_boxes', 'my_add_meta_box');
Vấn đề: dễ đụng tên hàm với plugin khác (namespace conflict), khó quản lý khi plugin lớn dần.
Cách chuẩn hơn — đóng gói bằng Class:
class My_Book_Manager {
public function __construct() {
add_action( 'init', [ $this, 'register_post_type' ] );
add_action( 'add_meta_boxes', [ $this, 'add_meta_boxes' ] );
add_action( 'save_post', [ $this, 'save_meta' ] );
}
public function register_post_type() {
// ...
}
public function add_meta_boxes() {
// ...
}
public function save_meta( $post_id ) {
// ...
}
}
new My_Book_Manager();
Lợi ích: tất cả hàm nằm trong 1 namespace (tên class) → gần như không bao giờ đụng tên với plugin khác. Đây là tư duy code có tổ chức, không chỉ “code chạy được”.
3. Activation / Deactivation / Uninstall — 3 khái niệm hay bị nhầm
// Chạy 1 LẦN DUY NHẤT khi user bấm "Activate" plugin
register_activation_hook( __FILE__, function() {
// VD: tạo custom table, flush rewrite rules
flush_rewrite_rules();
});
// Chạy khi user bấm "Deactivate" — TẠM NGƯNG, dữ liệu vẫn còn nguyên
register_deactivation_hook( __FILE__, function() {
flush_rewrite_rules();
});
// uninstall.php — file RIÊNG, đặt cùng cấp với file chính plugin
// Chỉ chạy khi user bấm "Delete" plugin hẳn trong wp-admin
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit; // chặn truy cập trực tiếp — bắt buộc có dòng này
}
// VD: xoá hẳn dữ liệu (options, custom table...)
delete_option( 'my_book_manager_settings' );
global $wpdb;
$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}my_books" );
Bảng phân biệt — câu hỏi hay gặp:
| Khi nào chạy | Dữ liệu | Dùng để làm gì | |
|---|---|---|---|
| Activation | Bấm “Activate” | Không đụng | Setup ban đầu (tạo table, set default option, flush rewrite) |
| Deactivation | Bấm “Deactivate” | Giữ nguyên | Dọn tạm thời (VD: bỏ lịch cron job) |
| Uninstall | Bấm “Delete” hẳn | Xoá vĩnh viễn | Dọn sạch hoàn toàn dữ liệu plugin để lại |
Lỗi hay gặp: nhét code xoá dữ liệu vào register_deactivation_hook — sai, vì user chỉ tắt tạm để test, không có ý xoá dữ liệu. Xoá dữ liệu chỉ được làm ở uninstall.php.
4. Vì sao flush_rewrite_rules() cần thiết khi đăng ký CPT
WordPress cache sẵn bảng “URL nào map với loại nội dung nào” (rewrite rules) để tăng tốc — không tính toán lại mỗi request. Khi bạn đăng ký CPT mới (thêm URL pattern mới), rules cũ không tự cập nhật → truy cập link CPT sẽ ra lỗi 404 cho tới khi rules được “flush” (tính lại).
⚠️ Lưu ý quan trọng: KHÔNG gọi flush_rewrite_rules() mỗi lần trang load (VD: trong hook init) — cực kỳ tốn hiệu năng vì đây là thao tác nặng. Chỉ gọi 1 lần lúc activate plugin, hoặc khi có thay đổi cấu trúc CPT thực sự.
PHẦN B: CUSTOM POST TYPE (CPT)
1. Đăng ký CPT — code đầy đủ + giải thích từng tham số quan trọng
add_action( 'init', function() {
register_post_type( 'book', [
'labels' => [
'name' => __( 'Sách', 'my-book-manager' ),
'singular_name' => __( 'Sách', 'my-book-manager' ),
'add_new_item' => __( 'Thêm sách mới', 'my-book-manager' ),
],
'public' => true, // hiển thị ra frontend + có UI trong admin
'show_in_rest' => true, // ⚠️ BẮT BUỘC true nếu muốn dùng Gutenberg editor hoặc REST API
'has_archive' => true, // có trang danh sách (archive-book.php)
'rewrite' => [ 'slug' => 'sach' ], // URL: /sach/ten-sach thay vì /book/ten-sach
'supports' => [ 'title', 'editor', 'thumbnail', 'excerpt' ],
'menu_icon' => 'dashicons-book',
'menu_position'=> 20,
] );
} );
Giải thích các tham số hay bị hỏi:
public: thực chất là “tổng hợp” của nhiều tham số con (publicly_queryable,show_ui,show_in_nav_menus,show_in_menu). Settruelà cách nhanh để bật hết; nếu cần control chi tiết (VD: có UI admin nhưng không public ra frontend) thì setpublic => falsevà khai báo riêng từng cái.show_in_rest: nếu đểfalsehoặc bỏ qua, CPT sẽ KHÔNG dùng được Gutenberg block editor (chỉ hiện editor cũ hoặc lỗi), và không có REST endpoint tự động (/wp-json/wp/v2/book). Đây là lỗi rất hay gặp khi nâng cấp code cũ lên dùng Gutenberg.supports: khai rõ tính năng nào post type này có. Thiếu'thumbnail'→ không thấy khung “Featured Image” trong admin dù đãadd_theme_support('post-thumbnails')ở theme.rewrite: custom slug URL. Sau khi đổi, phải flush rewrite rules (vào Settings → Permalinks bấm Save 1 lần, hoặc code flush lúc activate) mới có hiệu lực.
2. Custom Taxonomy — Hierarchical vs Non-hierarchical
add_action( 'init', function() {
// Hierarchical = giống Category (có cha-con, checkbox UI)
register_taxonomy( 'genre', 'book', [
'labels' => [ 'name' => __( 'Thể loại', 'my-book-manager' ) ],
'hierarchical' => true,
'show_in_rest' => true,
'rewrite' => [ 'slug' => 'the-loai' ],
] );
// Non-hierarchical = giống Tag (phẳng, UI dạng nhập tự do có gợi ý)
register_taxonomy( 'book_tag', 'book', [
'labels' => [ 'name' => __( 'Tag', 'my-book-manager' ) ],
'hierarchical' => false,
'show_in_rest' => true,
] );
} );
Câu hỏi: “Khi nào chọn hierarchical, khi nào chọn non-hierarchical?”
Trả lời: Hierarchical khi dữ liệu có quan hệ cha-con rõ ràng và số lượng term giới hạn, biết trước (VD: Thể loại sách: Văn học > Tiểu thuyết > Trinh thám). Non-hierarchical khi dữ liệu mở, người dùng tự thêm thoải mái, không có cấu trúc phân cấp (VD: Tag tự do).
3. Query theo Taxonomy
$query = new WP_Query([
'post_type' => 'book',
'tax_query' => [
[
'taxonomy' => 'genre',
'field' => 'slug',
'terms' => 'trinh-tham',
],
],
]);
PHẦN C: CUSTOM FIELDS — LÀM TAY VỚI META BOX
Đây là phần hay bị hỏi sâu nhất vì liên quan trực tiếp tới Security (nonce, sanitize, capability check).
1. Đăng ký Meta Box
add_action( 'add_meta_boxes', function() {
add_meta_box(
'book_author_meta', // ID duy nhất
__( 'Thông tin tác giả', 'my-book-manager' ), // Tiêu đề hiển thị
'render_book_author_meta_box', // Callback render HTML
'book', // Chỉ hiện ở post type 'book'
'side', // Vị trí: side / normal / advanced
'default' // Độ ưu tiên
);
} );
2. Render HTML — bắt buộc có Nonce field
function render_book_author_meta_box( $post ) {
// Nonce field — "chữ ký" xác nhận request này đến từ đúng form của mình
wp_nonce_field( 'save_book_author_meta', 'book_author_nonce' );
$author = get_post_meta( $post->ID, '_book_author', true );
?>
<label for="book_author_field">Tên tác giả</label>
<input
type="text"
id="book_author_field"
name="book_author_field"
value="<?php echo esc_attr( $author ); ?>"
style="width:100%"
/>
<?php
}
Vì sao esc_attr() bắt buộc ở đây: giá trị $author sẽ nằm trong thuộc tính value="" của HTML. Nếu tác giả nhập tên có ký tự " (dấu ngoặc kép), không escape sẽ làm hỏng cấu trúc HTML hoặc mở đường XSS. Quy tắc: bất cứ giá trị nào in ra HTML đều phải escape đúng ngữ cảnh (esc_attr cho attribute, esc_html cho nội dung text, esc_url cho link).
3. Xử lý Save — phần quan trọng nhất, dễ sai bảo mật nhất
add_action( 'save_post', function( $post_id ) {
// ① Check nonce — chống CSRF (kẻ xấu dụ admin submit form giả)
if ( ! isset( $_POST['book_author_nonce'] ) ||
! wp_verify_nonce( $_POST['book_author_nonce'], 'save_book_author_meta' ) ) {
return;
}
// ② Bỏ qua khi WordPress autosave (autosave không có $_POST đầy đủ, dễ gây lỗi/mất data)
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
// ③ Check quyền — user hiện tại có được sửa bài này không
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
// ④ Sanitize input trước khi lưu — KHÔNG BAO GIỜ lưu $_POST thô vào DB
if ( isset( $_POST['book_author_field'] ) ) {
$clean_value = sanitize_text_field( $_POST['book_author_field'] );
update_post_meta( $post_id, '_book_author', $clean_value );
}
} );
4 bước này là “công thức” bắt buộc phải thuộc — gần như mọi câu hỏi test về custom field/security đều xoay quanh đây:
| Bước | Thiếu thì sao? |
|---|---|
| ① Check nonce | Bị tấn công CSRF — trang khác có thể âm thầm submit form thay đổi data của bạn |
| ② Check autosave | Data có thể bị ghi đè sai hoặc lỗi PHP vì $_POST không đầy đủ lúc autosave |
③ Check current_user_can | User không đủ quyền vẫn có thể sửa được data (lỗ hổng phân quyền) |
| ④ Sanitize input | Mở đường SQL Injection / Stored XSS — dữ liệu độc lưu thẳng vào DB, phát tán khi hiển thị lại |
4. Hàm sanitize hay dùng theo loại dữ liệu
sanitize_text_field( $_POST['name'] ); // text thường, loại bỏ HTML/script tag
sanitize_email( $_POST['email'] ); // email
sanitize_textarea_field( $_POST['bio'] ); // textarea, giữ line break
absint( $_POST['age'] ); // ép về số nguyên dương
sanitize_key( $_POST['option_key'] ); // key/slug, chỉ giữ a-z0-9_-
wp_kses_post( $_POST['content'] ); // cho phép HTML an toàn (giống nội dung post)
PHẦN D: ACF (Advanced Custom Fields) — SO SÁNH VỚI LÀM TAY
Bảng so sánh — nên thuộc để trả lời câu “khi nào dùng ACF, khi nào code tay”
| Tiêu chí | Làm tay (meta box thủ công) | ACF |
|---|---|---|
| Tốc độ phát triển | Chậm hơn, viết nhiều code lặp | Nhanh, kéo-thả cấu hình field |
| Kiểm soát/tuỳ biến | Toàn quyền, tối ưu đúng ý | Bị giới hạn trong khả năng của plugin |
| Phụ thuộc | Không phụ thuộc plugin ngoài | Phụ thuộc ACF — nếu gỡ ACF, mất field hiển thị (dữ liệu vẫn còn trong DB) |
| Chi phí | Miễn phí hoàn toàn | Bản Pro trả phí cho tính năng nâng cao (Repeater, Flexible Content…) |
| Phù hợp | Sản phẩm dài hạn, cần tối ưu hiệu năng, ít field | Dự án cần triển khai nhanh, nhiều field, khách hàng tự quản trị nội dung |
Câu trả lời mẫu:
“Em ưu tiên ACF cho các field đơn giản, số lượng nhiều, cần bàn giao nhanh cho khách tự quản trị — vì tốc độ phát triển nhanh và UI thân thiện. Nhưng với các trường hợp cần tối ưu hiệu năng cao, hoặc logic phức tạp gắn liền với business logic riêng, em code tay để kiểm soát trọn vẹn, tránh phụ thuộc plugin ngoài.”
PHẦN E: THỰC HÀNH TAY — GHÉP TẤT CẢ LẠI
Viết đầy đủ 1 plugin từ đầu, cấu trúc file:
my-book-manager/
├── my-book-manager.php ← file chính, header + class chính
├── uninstall.php
└── includes/
└── class-book-cpt.php
Yêu cầu tự làm (không xem lại code mẫu trên, viết từ trí nhớ):
- Đăng ký CPT
bookvớishow_in_rest => true. - Đăng ký taxonomy
genre(hierarchical). - Thêm meta box “Tác giả” — đầy đủ 4 bước bảo mật ở Phần C.3.
- Test: tạo 1 sách, gán thể loại, nhập tên tác giả, save, F5 lại xem data còn không.
- Thử tấn công giả định: xoá nonce field khỏi HTML (dùng DevTools), submit lại form — phải bị chặn (không lưu được) → chứng minh nonce hoạt động.
PHẦN F: TỰ KIỂM TRA CUỐI NGÀY
- Phân biệt Activation / Deactivation / Uninstall — dữ liệu bị ảnh hưởng thế nào ở mỗi loại? (Activation/Deactivation giữ nguyên data, Uninstall xoá vĩnh viễn)
- Vì sao
show_in_rest => truebắt buộc phải có khi đăng ký CPT thời nay? (Cần cho Gutenberg editor + REST API endpoint tự động) - Kể đủ 4 bước bắt buộc khi xử lý
save_postcho custom field, thiếu bước nào gây hậu quả gì? (nonce → CSRF; autosave check → lỗi data; capability check → phân quyền; sanitize → injection/XSS) esc_attr()khácsanitize_text_field()ở điểm nào, dùng lúc nào? (sanitize khi LƯU vào DB, escape khi IN ra HTML — làm ở 2 thời điểm khác nhau)- Khi nào bạn chọn ACF thay vì code tay, và ngược lại?
Giáo trình ôn tập WordPress bài bản + Vibe Coding
Tác giả: Giang Thanh Nhon
Trạng thái: Đang tiến hành
Số chương: 7
Đánh giá: ⭐ 10/10
Hãy là người đầu tiên bình luận về chương này!