کلاس WP_Widget در وردپرس پایه ساخت ویجت‌های سفارشی است و به‌عنوان یکی از کلاس‌های بنیادین این سیستم، ساختاردهی منطق نمایش، فرم پیکربندی و ذخیره تنظیمات ویجت را ممکن می‌کند. بدون این کلاس، ساخت ویجت سفارشی نیازمند کدنویسی سطح پایین و مدیریت دستی hookها خواهد بود.

کلاس WP_Widget وردپرس یکی از پرکاربردترین کلاس‌های افزونه‌نویسی برای ساخت ویجت سفارشی است. این کلاس امکان تعریف متدهای construct، widget، form و update را فراهم می‌کند و پایه ساخت ویجت‌های حرفه‌ای در سایدبار و فوتر محسوب می‌شود. در این راهنما ساختار کامل، متدها، نمونه‌های واقعی، اشتباهات رایج و نکات امنیتی این کلاس بررسی می‌شود. همچنین تفاوت آن با بلاک‌های گوتنبرگ توضیح داده می‌شود. در پایان پرسش‌های پرتکرار و نگاه فنی عمیق به این کلاس مرور خواهد شد.

در پروژه‌هایی که سایدبار پویا یا ویجت‌های پیکربندی‌پذیر داشتند، این کلاس همیشه نقطه شروع بوده است. یک متد update بدون sanitize، به‌سادگی به یک نقص امنیتی تبدیل می‌شود و یک متد form بدون escape، رابط کاربری ناپایدار ایجاد می‌کند.

چرا کلاس WP_Widget اهمیت دارد

وردپرس از نسخه‌های ابتدایی خود یک سیستم ویجت ارائه داده که به کاربران اجازه می‌دهد اجزای مختلف را در سایدبار، فوتر یا سایر مناطق ویجت‌پذیر قرار دهند. این سیستم از دو بخش تشکیل شده: بخش مدیریت ویجت‌ها (که در پیشخوان است) و بخش نمایش آن‌ها در frontend.

کلاس WP_Widget این دو بخش را به هم متصل می‌کند. هر ویجت سفارشی که با این کلاس ساخته می‌شود، به‌طور خودکار در فهرست ویجت‌های پیشخوان ظاهر می‌شود و می‌تواند به هر منطقه ویجت‌پذیر کشیده شود. این یکپارچگی، همان چیزی است که افزونه‌نویسی را ساده می‌کند.

برای مطالعه سابقه تکاملی ویجت‌ها در وردپرس، مطلب ویجت‌های وردپرس از کلاسیک تا بلاک منبع خوبی است. همچنین برای مطالعه روش‌های جدید، مطلب مدیریت ویجت‌ها در قالب‌های مدرن راهنماست.

ساختار کلاس و متدهای اصلی

کلاس WP_Widget چهار متد اصلی دارد که هر ویجت سفارشی باید آن‌ها را پیاده‌سازی کند:

  • __construct(): تنظیم شناسه، نام و توضیحات ویجت
  • widget(): رندر خروجی در frontend
  • form(): رندر فرم پیکربندی در پیشخوان
  • update(): اعتبارسنجی و ذخیره تنظیمات

ساختار کلی یک ویجت سفارشی به این شکل است:

class My_Widget extends WP_Widget {
    public function __construct() {
        parent::__construct(
            'my_widget',
            __( 'ویجت من', 'my-plugin' ),
            array( 'description' => __( 'توضیح ویجت من', 'my-plugin' ) )
        );
    }

    public function widget( $args, $instance ) { /* رندر خروجی */ }
    public function form( $instance ) { /* فرم پیکربندی */ }
    public function update( $new_instance, $old_instance ) { /* ذخیره */ }
}

برای ثبت این ویجت، از hook widgets_init استفاده می‌شود. برای مطالعه بیشتر در مورد هوک‌ها، مطلب تابع register_widget راهنماست.

متد __construct و تنظیمات اولیه

متد __construct مسئول تنظیم پارامترهای اصلی ویجت است:

public function __construct() {
    parent::__construct(
        'my_unique_widget_id',
        __( 'عنوان ویجت', 'my-plugin' ),
        array(
            'description' => __( 'توضیح کوتاه', 'my-plugin' ),
            'classname'   => 'my-widget-class',
        )
    );
}

پارامتر اول، شناسه یکتای ویجت است که باید در سراسر سایت یکتا باشد. پارامتر دوم، عنوانی است که در پیشخوان نمایش داده می‌شود. پارامتر سوم آرایه‌ای از تنظیمات اضافی مثل توضیحات و کلاس CSS است.

نکته مهم در انتخاب ID

شناسه ویجت باید فقط شامل حروف کوچک لاتین و زیرخط باشد. اگر شناسه تکراری باشد، وردپرس ویجت جدید را ثبت نمی‌کند:

'my_unique_widget_id'  // درست
'my-widget'           // اشتباه، خط تیره ممکن است در برخی محیط‌ها مشکل‌ساز باشد

متد widget و رندر خروجی

متد widget مسئول تولید HTML نهایی در frontend است. این متد دو پارامتر دریافت می‌کند:

  • $args: شامل تنظیمات منطقه ویجت مثل before_widget و after_widget
  • $instance: شامل تنظیمات ذخیره‌شده کاربر
public function widget( $args, $instance ) {
    echo $args['before_widget'];

    if ( ! empty( $instance['title'] ) ) {
        echo $args['before_title'] . esc_html( $instance['title'] ) . $args['after_title'];
    }

    echo '<p>' . esc_html( $instance['content'] ) . '</p>';

    echo $args['after_widget'];
}

نکات کلیدی در این متد:

  • خروجی باید با esc_html، esc_url و wp_kses_post escape شود
  • استفاده از $args['before_widget'] و $args['after_widget'] برای سازگاری با قالب
  • همیشه مقادیر instance را با isset یا ! empty بررسی کنید

برای مطالعه جامع escape، مطلب Output Escaping در وردپرس راهنمای کامل است.

متد form و فرم پیکربندی

متد form فرم پیکربندی ویجت را در پیشخوان تولید می‌کند. این فرم در پنل ویجت‌ها زیر هر ویجت نمایش داده می‌شود:

public function form( $instance ) {
    $title   = isset( $instance['title'] ) ? $instance['title'] : '';
    $content = isset( $instance['content'] ) ? $instance['content'] : '';
    ?>
    <p>
        <label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
            <?php esc_html_e( 'عنوان', 'my-plugin' ); ?>
        </label>
        <input
            class="widefat"
            id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
            name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
            type="text"
            value="<?php echo esc_attr( $title ); ?>"
        />
    </p>
    <p>
        <label for="<?php echo esc_attr( $this->get_field_id( 'content' ) ); ?>">
            <?php esc_html_e( 'محتوا', 'my-plugin' ); ?>
        </label>
        <textarea
            class="widefat"
            id="<?php echo esc_attr( $this->get_field_id( 'content' ) ); ?>"
            name="<?php echo esc_attr( $this->get_field_name( 'content' ) ); ?>"
        ><?php echo esc_textarea( $content ); ?></textarea>
    </p>
    <?php
}

دو متد کمکی مهم در اینجا استفاده شده‌اند:

  • get_field_id(): تولید شناسه یکتا برای فیلدهای فرم
  • get_field_name(): تولید نام یکتا برای فیلدهای فرم

استفاده از این دو متد ضروری است چون اگر نام فیلدها تکراری باشند، داده‌های ویجت‌ها با هم تداخل می‌کنند.

متد update و ذخیره تنظیمات

متد update مسئول اعتبارسنجی و ذخیره داده‌های ورودی است. این متد دو پارامتر دریافت می‌کند:

  • $new_instance: مقادیر جدید که کاربر وارد کرده
  • $old_instance: مقادیر قبلی که در دیتابیس ذخیره شده
public function update( $new_instance, $old_instance ) {
    $instance = array();

    $instance['title']   = ! empty( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '';
    $instance['content'] = ! empty( $new_instance['content'] ) ? wp_kses_post( $new_instance['content'] ) : '';

    return $instance;
}

نکات کلیدی در این متد:

  • همیشه مقادیر را با توابع sanitize پاک کنید
  • خروجی همیشه باید آرایه‌ای از مقادیر تمیز باشد
  • از برگرداندن مقدار خالی یا false خودداری کنید

برای مطالعه جامع توابع sanitize، مطلب راهنمای Sanitization در وردپرس مرجع است.

نمونه‌های عملی در پروژه واقعی

ویجت کامل با عنوان و متن

class My_Text_Widget extends WP_Widget {

    public function __construct() {
        parent::__construct(
            'my_text_widget',
            __( 'ویجت متن سفارشی', 'my-plugin' ),
            array( 'description' => __( 'نمایش یک متن سفارشی', 'my-plugin' ) )
        );
    }

    public function widget( $args, $instance ) {
        echo $args['before_widget'];
        if ( ! empty( $instance['title'] ) ) {
            echo $args['before_title'] . esc_html( $instance['title'] ) . $args['after_title'];
        }
        echo '<div class="my-text-content">' . wp_kses_post( $instance['content'] ) . '</div>';
        echo $args['after_widget'];
    }

    public function form( $instance ) {
        $title   = isset( $instance['title'] ) ? $instance['title'] : '';
        $content = isset( $instance['content'] ) ? $instance['content'] : '';
        ?>
        <p>
            <label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">عنوان:</label>
            <input class="widefat" type="text"
                id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
                name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
                value="<?php echo esc_attr( $title ); ?>" />
        </p>
        <p>
            <label for="<?php echo esc_attr( $this->get_field_id( 'content' ) ); ?>">محتوا:</label>
            <textarea class="widefat"
                id="<?php echo esc_attr( $this->get_field_id( 'content' ) ); ?>"
                name="<?php echo esc_attr( $this->get_field_name( 'content' ) ); ?>"><?php echo esc_textarea( $content ); ?></textarea>
        </p>
        <?php
    }

    public function update( $new_instance, $old_instance ) {
        $instance = array();
        $instance['title']   = sanitize_text_field( $new_instance['title'] );
        $instance['content'] = wp_kses_post( $new_instance['content'] );
        return $instance;
    }
}

add_action( 'widgets_init', function () {
    register_widget( 'My_Text_Widget' );
} );

ویجت نمایش آخرین پست‌ها

public function widget( $args, $instance ) {
    $count = isset( $instance['count'] ) ? absint( $instance['count'] ) : 5;
    $posts = get_posts( array(
        'posts_per_page' => $count,
        'post_status'    => 'publish',
    ) );

    echo $args['before_widget'];
    echo $args['before_title'] . esc_html( $instance['title'] ) . $args['after_title'];
    echo '<ul>';
    foreach ( $posts as $post ) {
        printf(
            '<li><a href="%s">%s</a></li>',
            esc_url( get_permalink( $post ) ),
            esc_html( get_the_title( $post ) )
        );
    }
    echo '</ul>';
    echo $args['after_widget'];
}

در این الگو از تابع get_posts برای دریافت پست‌ها استفاده شده است.

ویجت نمایش اطلاعات کاربر

public function widget( $args, $instance ) {
    if ( ! is_user_logged_in() ) {
        return;
    }
    $current_user = wp_get_current_user();
    echo $args['before_widget'];
    printf(
        '<p>%s <strong>%s</strong></p>',
        esc_html__( 'خوش آمدید,', 'my-plugin' ),
        esc_html( $current_user->display_name )
    );
    echo $args['after_widget'];
}

ویجت با استفاده از Custom Post Type

برای نمایش post type سفارشی در ویجت، باید مطمئن شوید post type فعال است. مطلب تابع register_post_type راهنماست.

ویجت با فرم چندفیلدی پیشرفته

public function form( $instance ) {
    $count   = isset( $instance['count'] ) ? absint( $instance['count'] ) : 5;
    $orderby = isset( $instance['orderby'] ) ? $instance['orderby'] : 'date';
    $options = array( 'date' => 'تاریخ', 'title' => 'عنوان', 'rand' => 'تصادفی' );
    ?>
    <p>
        <label>تعداد نمایش:</label>
        <input type="number" min="1" max="20"
            name="<?php echo esc_attr( $this->get_field_name( 'count' ) ); ?>"
            value="<?php echo esc_attr( $count ); ?>" />
    </p>
    <p>
        <label>ترتیب:</label>
        <select name="<?php echo esc_attr( $this->get_field_name( 'orderby' ) ); ?>">
            <?php foreach ( $options as $key => $label ) : ?>
                <option value="<?php echo esc_attr( $key ); ?>" <?php selected( $orderby, $key ); ?>><?php echo esc_html( $label ); ?></option>
            <?php endforeach; ?>
        </select>
    </p>
    <?php
}

اشتباهات رایج در استفاده از WP_Widget

نبود فراخوانی parent::__construct

اگر در متد __construct فراخوانی parent::__construct را فراموش کنید، ویجت به‌درستی ثبت نمی‌شود و ممکن است خطای PHP رخ دهد.

نبود escape در متد widget

شایع‌ترین اشتباه امنیتی. هر مقداری که در frontend چاپ می‌شود باید escape شود. عدم escape به XSS منجر می‌شود.

نبود sanitize در متد update

دومین اشتباه امنیتی شایع. مقادیر ورودی کاربر باید با توابع sanitize پاک شوند، وگرنه داده مخرب در دیتابیس ذخیره می‌شود.

استفاده از ID تکراری

اگر دو ویجت با ID یکسان ثبت شوند، دومی ثبت نمی‌شود. همیشه از پیشوند اختصاصی استفاده کنید.

نبود استفاده از get_field_id و get_field_name

اگر نام فیلدها را دستی تعریف کنید، در چند ویجت با تنظیمات متفاوت، داده‌ها با هم تداخل می‌کنند:

// اشتباه
<input name="title" />

// درست
<input name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" />

نبود بررسی وجود ویجت

اگر ویجت را با نام کلاس ثبت کنید و کلاس در زمان ثبت تعریف نشده باشد، خطا رخ می‌دهد:

add_action( 'widgets_init', function () {
    if ( class_exists( 'My_Text_Widget' ) ) {
        register_widget( 'My_Text_Widget' );
    }
} );

نبود تست روی سناریوهای مرزی

تست‌هایی مثل «ویجت با مقادیر خالی»، «ویجت با کاراکتر یونیکد»، «چند ویجت با یک کلاس» و «ویجت در چند منطقه» را حتماً بنویسید.

امنیت و عملکرد در WP_Widget

این کلاس به‌تنهایی امنیت را تأمین نمی‌کند و در ترکیب با لایه‌های زیر معنا پیدا می‌کند:

  • sanitize_callback در متد update برای ورودی‌ها
  • escape کامل در متد widget برای خروجی‌ها
  • escape در متد form برای مقادیر نمایش‌داده‌شده
  • بررسی capability کاربر در ذخیره تنظیمات (وردپرس به‌طور خودکار انجام می‌دهد)

برای مطالعه جامع مباحث امنیتی، مطلب SQL Injection Prevention در وردپرس و راهنمای Sanitization مرجع هستند.

از نظر عملکرد، هر ویجت در زمان رندر سایدبار یک متد widget اجرا می‌کند. اگر این متد کوئری سنگین اجرا کند، به‌ازای هر بازدید صفحه یک کوئری اضافه انجام می‌شود. توصیه می‌شود:

  1. نتیجه کوئری‌های پرتکرار را در cache ذخیره کنید
  2. از get_transient و set_transient برای داده‌های تازه استفاده کنید
  3. در ویجت‌های سنگین، از بلاک‌های گوتنبرگ با Server Side Rendering استفاده کنید

برای مطالعه الگوهای بهینه، مطلب تابع get_transient و تابع set_transient راهنماست.

پرسش‌های پرتکرار درباره WP_Widget

تفاوت WP_Widget با بلاک گوتنبرگ چیست؟

WP_Widget رویکرد کلاسیک است که در پنل ویجت‌ها و Customizer استفاده می‌شود، در حالی که بلاک‌های گوتنبرگ رویکرد جدید و مبتنی بر ویرایشگر بلاک هستند. برای پروژه‌های جدید، ترکیب هر دو (سازگاری) توصیه می‌شود.

چرا ویجت من در پیشخوان ظاهر نمی‌شود؟

معمولاً به سه دلیل: فراخوانی register_widget در hook اشتباه، نبود parent::__construct در سازنده، یا ID تکراری.

آیا می‌توان ویجت را در Customizer نمایش داد؟

بله، وردپرس به‌طور خودکار ویجت‌های کلاسیک را در Customizer نیز نمایش می‌دهد اگر قالب از add_theme_support( 'widgets' ) پشتیبانی کند.

آیا می‌توان ویجت را با JS داینامیک کرد؟

بله، اما باید فایل JS را در پیشخوان بارگذاری کنید تا در زمان ویرایش ویجت فعال باشد. مطلب هوک admin_enqueue_scripts راهنماست.

آیا WP_Widget با PHP 8.x سازگار است؟

بله، از نسخه‌های جدید وردپرس، این کلاس با PHP 8.x سازگار شده است. اما اگر افزونه شما از الگوهای قدیمی استفاده می‌کند، بررسی مجدد توصیه می‌شود.

آیا می‌توان ویجت را در صفحه‌های دیگر (غیر از سایدبار) نمایش داد؟

بله، با فراخوانی the_widget() در هر جای قالب. این تابع ویجت را در نقطه دلخواه رندر می‌کند.

آیا ویجت‌های کلاسیک در آینده حذف می‌شوند؟

تا کنون حذف نشده‌اند و همچنان پشتیبانی می‌شوند. اما روند وردپرس به‌سمت بلاک‌هاست. توصیه می‌شود پروژه‌های جدید از ترکیب کلاسیک و بلاک استفاده کنند.

نگاه فنی عمیق به WP_Widget

در سطح معماری، WP_Widget یک کلاس انتزاعی است که در فایل wp-includes/class-wp-widget.php تعریف شده. هر ویجت سفارشی، این کلاس را ارث‌بری می‌کند و متدهای آن را پیاده‌سازی می‌کند. وردپرس از یک الگوی Registry برای مدیریت ویجت‌ها استفاده می‌کند که در متغیر سراسری $wp_registered_widgets نگهداری می‌شود.

نکته ظریف اول، مسئله Serialization است. مقادیر ویجت به‌صورت serialized در گزینه‌های وردپرس ذخیره می‌شوند. اگر در متد update آرایه‌ای با ساختار پیچیده برگردانید، ممکن است در نسخه‌های بعدی PHP خطای unserialize رخ دهد. برای همین توصیه می‌شود ساختار داده ساده نگه داشته شود.

نکته دوم، تعامل با Cache است. نتایج ویجت به‌طور خودکار کش نمی‌شوند. اگر متد widget کوئری سنگین داشته باشد، هر بار رندر سایدبار یک کوئری انجام می‌شود. راهکار استاندارد استفاده از wp_cache_get و wp_cache_set یا Transient است.

مسئله سوم، رفتار ویجت‌های کلاسیک در قالب‌های بلاکی (FSE) است. در قالب‌های جدید که از Site Editor استفاده می‌کنند، ویجت‌های کلاسیک به‌طور پیش‌فرض پشتیبانی نمی‌شوند. اگر قالب شما FSE است، باید بلاک‌های معادل بسازید یا از ویجت‌های کلاسیک در یک منطقه سازگار استفاده کنید. مطلب راهنمای Full Site Editing این موضوع را پوشش می‌دهد.

در نهایت، در پروژه‌های Enterprise توصیه می‌شود به‌جای ساخت ویجت‌های متنوع، یک ویجت Generic با فیلدهای قابل تنظیم بسازید که از یک رجیستری داخلی برای انواع مختلف استفاده کند. این کار از تکرار کد جلوگیری می‌کند و نگهداری را ساده‌تر می‌کند. برای مطالعه بیشتر، مباحث ساخت ویجت سفارشی حرفه‌ای و WordPress Components مفید هستند. برای مطالعه بیشتر درباره خود وردپرس، WordPress در ویکی‌پدیا نقطه شروع خوبی است.

اگر در پروژه‌ای با مشکل مهاجرت ویجت‌های کلاسیک به بلاک یا رفتار غیرمنتظره در ذخیره مقادیر مواجه شده‌اید، برای ما جالب است بدانید کدام راهکار عملاً به حل مسئله کمک کرده است. تجربه خود را در دیدگاه‌ها بنویسید تا برای سایر توسعه‌دهندگان هم مفید باشد.