🔌 فصل ۸: توسعه افزونه وردپرس
افزونهها نسبت به قالبها قابل حملتر هستن - با هر قالبی کار میکنن. توی این فصل از صفر یه افزونه کامل میسازیم.
افزونهها نسبت به قالبها قابل حملتر هستن – با هر قالبی کار میکنن. توی این فصل از صفر یه افزونه کامل میسازیم.
🤔 چرا افزونه بنویسیم؟
تجربه ساخت Persian Donate Plus رو داری – میدونی چقدر مفیده. افزونه برای این موارد:
- قابلیتهایی که با تغییر قالب نباید از بین بره
- محصولی که میخوای بفروشی یا منتشر کنی
- قابلیتهای مشترک بین چند سایت
- یکپارچگی با سرویسهای دیگه (درگاه پرداخت، API)
- تغییر رفتار وردپرس یا افزونههای دیگه
📁 ساختار افزونه
افزونه ساده (تکفایل)
برای کارهای کوچک:
wp-content/plugins/
└── my-plugin.php
افزونه پیچیده (پوشهای)
wp-content/plugins/my-plugin/
├── my-plugin.php # فایل اصلی (header افزونه)
├── readme.txt # توضیحات (برای WordPress.org)
├── uninstall.php # هنگام حذف افزونه
├── languages/ # ترجمهها
├── includes/ # کلاسها و توابع
│ ├── class-my-plugin.php
│ ├── class-admin.php
│ └── class-public.php
├── admin/ # صفحات ادمین
│ ├── views/
│ ├── css/
│ └── js/
├── public/ # فرانت
│ ├── css/
│ └── js/
└── assets/ # تصاویر، آیکون
👋 اولین افزونه – Hello World
قدم ۱: ساخت فایل
یه پوشه به اسم my-first-plugin در wp-content/plugins/ بساز و یه فایل PHP درش بساز:
<?php
/**
* Plugin Name: اولین افزونه من
* Plugin URI: https://icsd.ir/my-first-plugin
* Description: یه افزونه ساده برای یاد گرفتن
* Version: 1.0.0
* Requires at least: 6.0
* Requires PHP: 7.4
* Author: محمدعلی ناظری
* Author URI: https://icsd.ir
* 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_VERSION', '1.0.0' );
define( 'MFP_PATH', plugin_dir_path( __FILE__ ) );
define( 'MFP_URL', plugin_dir_url( __FILE__ ) );
// تابع ساده
function mfp_say_hello() {
echo '<p style="background:#2271b1;color:#fff;padding:20px;text-align:center;">سلام از طرف اولین افزونه!</p>';
}
// نمایش در فوتر
add_action( 'wp_footer', 'mfp_say_hello' );
قدم ۲: فعالسازی
برو به افزونهها در پنل وردپرس. افزونه جدید رو میبینی – فعالش کن. حالا توی فوتر همه صفحات اون پیام رو میبینی!
اولین افزونهت کار میکنه. حالا بریم سراغ یه افزونه واقعی.
⭐ افزونه واقعی – دیدگاه ویژه
یه افزونه میسازیم که به دیدگاهها امکان «ویژه شدن» میده.
ساختار
wp-content/plugins/featured-comments/
├── featured-comments.php
├── readme.txt
└── assets/
└── css/
└── style.css
فایل اصلی
<?php
/**
* Plugin Name: دیدگاه ویژه
* Description: امکان نشانهگذاری دیدگاهها به عنوان ویژه
* Version: 1.0.0
* Author: محمدعلی ناظری
* Text Domain: featured-comments
*/
if ( ! defined( 'ABSPATH' ) ) exit;
class Featured_Comments {
public function __construct() {
// اضافه کردن لینک «ویژهسازی» به ردیف هر دیدگاه در ادمین
add_filter( 'comment_row_actions', array( $this, 'add_featured_action' ), 10, 2 );
// پردازش لینک کلیکشده
add_action( 'admin_init', array( $this, 'handle_featured_action' ) );
// اضافه کردن کلاس CSS به دیدگاههای ویژه
add_filter( 'comment_class', array( $this, 'add_featured_class' ), 10, 4 );
// لود استایل
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_styles' ) );
}
/**
* اضافه کردن لینک به ردیف دیدگاه در ادمین
*/
public function add_featured_action( $actions, $comment ) {
$is_featured = get_comment_meta( $comment->comment_ID, '_featured', true );
$url = wp_nonce_url(
add_query_arg( array(
'action' => 'toggle_featured',
'comment_id' => $comment->comment_ID,
), admin_url( 'edit-comments.php' ) ),
'fc_toggle_' . $comment->comment_ID
);
$label = $is_featured ? 'لغو ویژه' : '⭐ ویژهسازی';
$actions['featured'] = '<a href="' . esc_url( $url ) . '">' . $label . '</a>';
return $actions;
}
/**
* پردازش کلیک
*/
public function handle_featured_action() {
if ( ! isset( $_GET['action'] ) || $_GET['action'] !== 'toggle_featured' ) {
return;
}
if ( ! isset( $_GET['comment_id'] ) ) {
return;
}
$comment_id = absint( $_GET['comment_id'] );
// بررسی nonce
if ( ! wp_verify_nonce( $_GET['_wpnonce'], 'fc_toggle_' . $comment_id ) ) {
wp_die( 'دسترسی غیرمجاز' );
}
// بررسی دسترسی
if ( ! current_user_can( 'moderate_comments' ) ) {
wp_die( 'دسترسی کافی ندارید' );
}
// toggle
$is_featured = get_comment_meta( $comment_id, '_featured', true );
if ( $is_featured ) {
delete_comment_meta( $comment_id, '_featured' );
} else {
update_comment_meta( $comment_id, '_featured', '1' );
}
// برگردون
wp_safe_redirect( admin_url( 'edit-comments.php' ) );
exit;
}
/**
* اضافه کردن کلاس به دیدگاه ویژه
*/
public function add_featured_class( $classes, $css_class, $comment_id, $comment ) {
if ( get_comment_meta( $comment_id, '_featured', true ) ) {
$classes[] = 'comment-featured';
}
return $classes;
}
/**
* لود استایل
*/
public function enqueue_styles() {
wp_enqueue_style(
'featured-comments',
plugin_dir_url( __FILE__ ) . 'assets/css/style.css',
array(),
'1.0.0'
);
}
}
// راهاندازی
new Featured_Comments();
استایل
.comment-featured {
background: linear-gradient(135deg, #fef3c7, #fde68a);
border-right: 4px solid #f59e0b;
padding: 16px;
border-radius: 8px;
position: relative;
}
.comment-featured::before {
content: '⭐ دیدگاه ویژه';
position: absolute;
top: 8px;
left: 8px;
background: #f59e0b;
color: white;
padding: 4px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: bold;
}
🎬 Activation و Deactivation
وقتی افزونه رو فعال یا غیرفعال میکنی، میتونی کارهایی انجام بدی:
// فعالسازی
function my_plugin_activate() {
// ساخت جدول دیتابیس
global $wpdb;
$table_name = $wpdb->prefix . 'my_data';
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE $table_name (
id bigint(20) NOT NULL AUTO_INCREMENT,
name varchar(255) NOT NULL,
value text,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) $charset_collate;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );
// مقدار پیشفرض تنظیمات
add_option( 'my_plugin_settings', array(
'enabled' => true,
'mode' => 'production',
) );
// flush rewrite (در صورت ساخت CPT)
flush_rewrite_rules();
}
register_activation_hook( __FILE__, 'my_plugin_activate' );
// غیرفعالسازی
function my_plugin_deactivate() {
// پاک کردن کرونها
wp_clear_scheduled_hook( 'my_plugin_cron' );
flush_rewrite_rules();
}
register_deactivation_hook( __FILE__, 'my_plugin_deactivate' );
uninstall.php
وقتی کاربر افزونه رو حذف کامل میکنه (نه فقط غیرفعال):
<?php
// جلوگیری از اجرا بهجز هنگام uninstall
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
// حذف option ها
delete_option( 'my_plugin_settings' );
// حذف جدول
global $wpdb;
$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}my_data" );
// حذف meta
$wpdb->query( "DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE '_my_plugin_%'" );
📋 صفحه ادمین
// اضافه کردن منو در ادمین
function my_plugin_admin_menu() {
// منوی اصلی
add_menu_page(
'تنظیمات افزونه من', // page title
'افزونه من', // menu title
'manage_options', // capability
'my-plugin', // slug
'my_plugin_admin_page', // callback
'dashicons-admin-generic', // icon
100 // position
);
// زیرمنو
add_submenu_page(
'my-plugin',
'گزارشها',
'گزارشها',
'manage_options',
'my-plugin-reports',
'my_plugin_reports_page'
);
}
add_action( 'admin_menu', 'my_plugin_admin_menu' );
function my_plugin_admin_page() {
// ذخیره تنظیمات
if ( isset( $_POST['save_settings'] ) ) {
check_admin_referer( 'my_plugin_settings' );
$settings = array(
'enabled' => isset( $_POST['enabled'] ),
'mode' => sanitize_text_field( $_POST['mode'] ?? '' ),
);
update_option( 'my_plugin_settings', $settings );
echo '<div class="notice notice-success"><p>تنظیمات ذخیره شد!</p></div>';
}
$settings = get_option( 'my_plugin_settings', array() );
?>
<div class="wrap">
<h1>تنظیمات افزونه من</h1>
<form method="post">
<?php wp_nonce_field( 'my_plugin_settings' ); ?>
<table class="form-table">
<tr>
<th>فعال؟</th>
<td>
<input type="checkbox" name="enabled"
<?php checked( $settings['enabled'] ?? false ); ?>>
</td>
</tr>
<tr>
<th>حالت</th>
<td>
<select name="mode">
<option value="dev" <?php selected( $settings['mode'] ?? '', 'dev' ); ?>>توسعه</option>
<option value="production" <?php selected( $settings['mode'] ?? '', 'production' ); ?>>انتشار</option>
</select>
</td>
</tr>
</table>
<?php submit_button( 'ذخیره تنظیمات', 'primary', 'save_settings' ); ?>
</form>
</div>
<?php
}
🔒 امنیت در افزونهنویسی
هر ورودی کاربر یا داده خارجی پاکسازی، هر خروجی escape، و هر اقدام مهم nonce داشته باشه.
۱. Nonce برای CSRF
// در فرم
wp_nonce_field( 'my_action_name', '_my_nonce' );
// در پردازش
if ( ! isset( $_POST['_my_nonce'] ) ||
! wp_verify_nonce( $_POST['_my_nonce'], 'my_action_name' ) ) {
wp_die( 'درخواست نامعتبر' );
}
۲. پاکسازی ورودی
$text = sanitize_text_field( $_POST['name'] );
$email = sanitize_email( $_POST['email'] );
$url = esc_url_raw( $_POST['website'] );
$int = absint( $_POST['count'] );
$html = wp_kses_post( $_POST['content'] ); // اجازه HTML محدود
$key = sanitize_key( $_POST['key'] );
$slug = sanitize_title( $_POST['slug'] );
۳. Escape کردن خروجی
echo esc_html( $title ); // برای متن
echo esc_attr( $value ); // برای attribute
echo esc_url( $link ); // برای href/src
echo esc_textarea( $content ); // برای textarea
echo esc_js( $script_value ); // برای JavaScript
echo wp_kses_post( $rich_text ); // برای HTML امن
۴. بررسی دسترسی
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'دسترسی کافی ندارید' );
}
// بقیه دسترسیها:
// edit_posts, publish_posts, delete_posts, upload_files,
// moderate_comments, edit_users, install_plugins
۵. Prepared Statement برای دیتابیس
global $wpdb;
// ❌ غلط - SQL injection
$result = $wpdb->get_results( "SELECT * FROM table WHERE id = $id" );
// ✅ درست
$result = $wpdb->get_results( $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}my_table WHERE id = %d",
$id
) );
// با چند پارامتر
$wpdb->prepare(
"SELECT * FROM table WHERE name = %s AND age > %d",
$name, $age
);
📤 انتشار در WordPress.org
تجربه انتشار Persian Donate Plus رو داری! مراحل کلی:
- افزونه رو با استانداردهای WordPress.org Coding Standards بنویس
- یه
readme.txtبا فرمت استاندارد بساز - بنر و آیکون اضافه کن (در پوشه assets)
- ثبتنام در wordpress.org/plugins
- ارسال افزونه برای بررسی
- بعد از تأیید (۲ روز تا ۲ هفته)، دسترسی SVN دریافت میکنی
- کد رو commit کن و tag بزن
نمونه readme.txt
=== Plugin Name ===
Contributors: yourname
Tags: comments, featured, custom
Requires at least: 6.0
Tested up to: 6.5
Requires PHP: 7.4
Stable tag: 1.0.0
License: GPLv2 or later
A short description in 150 chars.
== Description ==
Detailed description here...
== Installation ==
1. Upload to /wp-content/plugins/
2. Activate
3. Configure
== Changelog ==
= 1.0.0 =
* Initial release
📝 خلاصه فصل
- افزونه باید header استاندارد داشته باشه
- register_activation_hook برای راهاندازی
- uninstall.php برای پاکسازی هنگام حذف
- هر ورودی sanitize، هر خروجی escape
- nonce برای جلوگیری از CSRF
- $wpdb->prepare برای جلوگیری از SQL injection