«چرا کد من اجرا نمی‌شود؟» — این سؤال را سال‌ها پیش از یک توسعه‌دهنده تازه‌کار شنیدم که یک فیلتر روی the_content ثبت کرده بود ولی هیچ تغییری در سایت نمی‌دید. سه ساعت وقت گذاشته بود، لاگ گذاشته بود، متغیرها را چاپ کرده بود. وقتی فایلش را باز کردم، در پنج ثانیه فهمیدم مشکل چیست: او نمی‌دانست آیا هوکی که روی آن کار می‌کند، اصلاً در آن نقطه از چرخه اجرا وجود دارد یا نه. آن روز یک عادت در من شکل گرفت که تا امروز در هر پروژه‌ای همراه من است: پیش از نوشتن callback، وجود هوک را بررسی کن؛ پیش از دیباگ، وجود callback را چک کن. این مقاله، همان عادت و ابزارهایش است. اگر با مفاهیم پایه آشنا نیستید، پیش از ادامه هوک‌های وردپرس چیست، تفاوت اکشن و فیلتر در وردپرس و نحوه استفاده از add_action را بخوانید. مکمل این مقاله حذف اکشن هوک، حذف فیلتر هوک، Priority در هوک‌ها و راهنمای حرفه‌ای کار با هوک‌ها است.

چرا بررسی وجود هوک، یک مهارت پایه است؟

در توسعه وردپرس، سه سناریوی بنیادین وجود دارد که بدون بررسی وجود هوک، به ساعت‌ها دیباگ منتهی می‌شود:

  1. افزونه‌ای که انتظار دارید هوکی را ثبت کرده باشد، ثبت نکرده: در این حالت، callback شما هرگز اجرا نمی‌شود و شما نمی‌دانید مشکل از کجاست.
  2. می‌خواهید هوکی را حذف کنید ولی نمی‌دانید اصلاً وجود دارد یا نه: remove_action روی هوک غیرموجود، خطا نمی‌دهد ولی کاری هم نمی‌کند.
  3. می‌خواهید شرطی روی یک هوک بگذارید: مثلاً «اگر افزونهٔ X فعال است و هوک Y را دارد، کاری بکن».

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

پیش از هر دیباگ، اول بپرس «آیا هوک وجود دارد؟»؛ اگر جواب بله باشد، سراغ «آیا callback ثبت شده؟» برو. این ترتیب، نصف وقت دیباگ را صرفه‌جویی می‌کند.

توابع پایه بررسی وجود هوک: has_action و has_filter

وردپرس دو تابع اختصاصی برای بررسی وجود callback روی یک هوک دارد:

has_action( $hook_name, $callback = false );
has_filter( $hook_name, $callback = false );

سه حالت استفاده:

حالت اول — بررسی وجود خود هوک: اگر پارامتر دوم را ندهید، بررسی می‌کند که آیا هوکی به این نام ثبت شده یا نه:

if ( has_action( 'myplugin_after_order_created' ) ) {
    // حداقل یک callback روی این اکشن ثبت شده است
}

if ( has_filter( 'the_content' ) ) {
    // حداقل یک callback روی این فیلتر ثبت شده است
}

حالت دوم — بررسی وجود یک callback خاص: اگر پارامتر دوم را بدهید، بررسی می‌کند که آیا آن callback مشخص روی هوک ثبت شده یا نه. خروجی، یا false است یا عدد priority ثبت‌شده:

$priority = has_action( 'init', 'myplugin_register_post_type' );
if ( false !== $priority ) {
    // ثبت شده است؛ priority آن در $priority است
}

$priority = has_filter( 'the_content', 'myplugin_modify_content' );
if ( false !== $priority ) {
    // ثبت شده؛ priority موجود در $priority
}

حالت سوم — بررسی متد کلاس: اگر callback متد کلاس است، به‌صورت آرایه پاس دهید:

if ( has_action( 'init', array( 'My_Plugin', 'register_post_type' ) ) ) {
    // متد کلاس ثبت شده است
}

if ( has_filter( 'the_content', array( $instance, 'modify_content' ) ) ) {
    // متد نمونه ثبت شده است
}

نکتهٔ ظریف: has_action در واقع یک wrapper روی has_filter است. این دو تابع، از نظر پیاده‌سازی یکسان هستند و خروجی هر دو، عدد priority یا false است. راهنمای کامل در نحوه استفاده از add_action، نحوه استفاده از add_filter، حذف اکشن هوک، حذف فیلتر هوک و Priority در هوک‌ها آمده است.

تفاوت has_action و did_action

یکی از سردرگمی‌های رایج، تفاوت بین has_action و did_action است:

  • has_action: بررسی می‌کند که آیا callback روی یک هوک ثبت شده است یا نه. این تابع، به لحظهٔ ثبت کار دارد، نه به لحظهٔ اجرا.
  • did_action: بررسی می‌کند که آیا یک اکشن اجرا شده است یا نه. خروجی، تعداد دفعات اجراست. اگر صفر بود، یعنی هنوز اجرا نشده.
// بررسی ثبت شدن
if ( has_action( 'init', 'myplugin_callback' ) ) {
    // callback ثبت شده است
}

// بررسی اجرا شدن
$count = did_action( 'init' );
if ( $count > 0 ) {
    // init حداقل یک بار اجرا شده
    error_log( sprintf( 'init %d بار اجرا شده است', $count ) );
}

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

if ( 0 === did_action( 'init' ) ) {
    // init هنوز اجرا نشده؛ کد خود را برای بعد ذخیره کنید
    add_action( 'init', 'myplugin_deferred_callback' );
} else {
    // init اجرا شده؛ کد را بلافاصله اجرا کنید
    myplugin_run_now();
}

این الگو، در پروژه‌هایی که افزونه باید در زمان‌های مختلف بارگذاری شود (گاهی پیش از init، گاهی بعد از آن)، بسیار کاربرد دارد. راهنمای تکمیلی در هوک‌های وردپرس، کنترل ترتیب اجرای هوک‌ها، توابع دیباگ وردپرس و راهنمای حرفه‌ای هوک‌ها آمده است.

has_action می‌پرسد «آیا کسی هست؟»؛ did_action می‌پرسد «آیا کسی آمده؟». اولی درباره آیندهٔ برنامه است، دومی درباره گذشته.

بررسی هوک جاری با current_action و current_filter

در callbackهای هوک، دو تابع به شما می‌گویند «الان در کدام هوک هستم»:

add_action( 'init', 'myplugin_shared_callback' );
add_action( 'admin_init', 'myplugin_shared_callback' );

function myplugin_shared_callback() {
    $current = current_action();
    // در init: init — در admin_init: admin_init
    if ( 'init' === $current ) {
        // منطق مخصوص init
    }
}

تفاوت current_action و current_filter: هر دو یکسان عمل می‌کنند و نام هوک جاری را برمی‌گردانند. current_action برای اکشن‌ها و current_filter برای فیلترها نام‌گذاری شده، ولی در وردپرس، این دو، wrapper یکدیگرند.

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

function myplugin_on_plugin_activation( $network_wide ) {
    $hook = current_filter();
    if ( 'activate_my-plugin/my-plugin.php' === $hook ) {
        // فعال‌سازی از پیشخوان
    } elseif ( 'activate_network_my-plugin/my-plugin.php' === $hook ) {
        // فعال‌سازی شبکه‌ای
    }
}

راهنمای کامل در هوک‌های وردپرس، استفاده درست از هوک‌ها، تفاوت اکشن و فیلتر، مهم‌ترین اکشن هوک‌ها و مهم‌ترین فیلتر هوک‌ها آمده است.

بررسی هوک با global $wp_filter

ابزار قدرتمند بعدی، متغیر سراسری $wp_filter است که تمام callbackهای ثبت‌شده روی تمام هوک‌ها را نگه می‌دارد:

global $wp_filter;

// بررسی وجود یک هوک خاص
if ( isset( $wp_filter['the_content'] ) ) {
    // هوک the_content وجود دارد و آرایه callbackهای ثبت‌شده را دارد
}

// شمارش تعداد callbackهای ثبت‌شده روی یک هوک
if ( isset( $wp_filter['the_content'] ) ) {
    $count = $wp_filter['the_content']->callbacks;
    // $count یک آرایه با ساختار priority => [callback, ...] است
}

ساختار داخلی $wp_filter['hook_name']:

WP_Hook Object
(
    [callbacks] => Array
        (
            [10] => Array
                (
                    [myplugin_callback_1] => Array
                        (
                            [function] => myplugin_callback_1
                            [accepted_args] => 1
                        )
                    [myplugin_callback_2] => Array
                        (
                            [function] => Array
                                (
                                    [0] => My_Class Object
                                    [1] => my_method
                                )
                            [accepted_args] => 2
                        )
                )
        )
)

الگوی کاربردی برای دیباگ: چاپ تمام callbackهای یک هوک با priority و نام تابع:

function myplugin_debug_hook( $hook_name ) {
    global $wp_filter;
    
    if ( ! isset( $wp_filter[ $hook_name ] ) ) {
        error_log( sprintf( 'هوک %s ثبت نشده است', $hook_name ) );
        return;
    }
    
    $hook = $wp_filter[ $hook_name ];
    error_log( sprintf( 'هوک: %s — مجموع callbackها: %d', $hook_name, $hook->callbacks ? array_sum( array_map( 'count', $hook->callbacks ) ) : 0 ) );
    
    foreach ( $hook->callbacks as $priority => $callbacks ) {
        foreach ( $callbacks as $id => $data ) {
            if ( is_string( $data['function'] ) ) {
                $name = $data['function'];
            } elseif ( is_array( $data['function'] ) ) {
                $class = is_object( $data['function'][0] ) ? get_class( $data['function'][0] ) : $data['function'][0];
                $name = $class . '::' . $data['function'][1];
            } else {
                $name = '(closure)';
            }
            error_log( sprintf( '  - priority %d: %s', $priority, $name ) );
        }
    }
}

این تابع، در پروژه‌های واقعی، بارها به من کمک کرده تا مقصرِ تعارض یا ترتیب اشتباه را در چند دقیقه پیدا کنم. یک تجربه میدانی: در پروژه‌ای، صفحهٔ محصول یک بار اضافه رندر می‌شد. با $wp_filter، کشف کردیم که دو افزونه هم‌زمان روی woocommerce_single_product_summary با priority یکسان callback ثبت کرده‌اند و ترتیب اجرا تصادفی است. تنظیم priority، مشکل را حل کرد. راهنمای کامل در دیباگ اکشن و فیلتر، توابع دیباگ وردپرس، دیباگ کد سفارشی وردپرس، تست و دیباگ پروژه‌های وردپرس و کنترل ترتیب اجرای هوک‌ها آمده است.

global $wp_filter مثل یک دفتر تلفن است: هر کسی که روی هوکی ثبت شده، در آن دفتر ثبت شده است. اگر نتوانی کسی را پیدا کنی، یعنی اصلاً ثبت نشده.

کاربردهای عملی بررسی هوک در پروژه‌های واقعی

بررسی وجود هوک، در پروژه‌های واقعی، در پنج سناریوی خاص کاربرد دارد:

سناریو اول — بررسی سازگاری پیش از اجرای کد وابسته:

function myplugin_init_features() {
    // اگر افزونه ووکامرس فعال است و هوک موردنظر وجود دارد
    if ( has_action( 'woocommerce_thankyou' ) ) {
        add_action( 'woocommerce_thankyou', 'myplugin_sync_inventory' );
    }
    
    // اگر افزونهٔ LMS فعال است
    if ( has_filter( 'learndash_course_content' ) ) {
        add_filter( 'learndash_course_content', 'myplugin_add_badge' );
    }
}
add_action( 'plugins_loaded', 'myplugin_init_features' );

سناریو دوم — حذف هوک شرطی:

function myplugin_conditional_remove() {
    if ( has_action( 'wp_footer', 'other_plugin_add_banner' ) ) {
        remove_action( 'wp_footer', 'other_plugin_add_banner', 10 );
    }
}
add_action( 'wp_loaded', 'myplugin_conditional_remove' );

سناریو سوم — جلوگیری از ثبت تکراری:

function myplugin_register_callback() {
    if ( ! has_action( 'init', 'myplugin_do_something' ) ) {
        add_action( 'init', 'myplugin_do_something' );
    }
}

سناریو چهارم — بررسی بارگذاری پیش از زمان:

function myplugin_ensure_hook_ready() {
    if ( 0 === did_action( 'init' ) ) {
        // init هنوز اجرا نشده؛ callback را ثبت کن
        add_action( 'init', 'myplugin_setup' );
    } else {
        // init اجرا شده؛ بلافاصله اجرا کن
        myplugin_setup();
    }
}

سناریو پنجم — دیباگ تعارض افزونه‌ها:

function myplugin_report_hook_conflicts() {
    global $wp_filter;
    
    $critical_hooks = array( 'the_content', 'save_post', 'wp_head', 'wp_footer' );
    
    foreach ( $critical_hooks as $hook ) {
        if ( ! isset( $wp_filter[ $hook ] ) ) {
            continue;
        }
        $count = $wp_filter[ $hook ]->callbacks ? array_sum( array_map( 'count', $wp_filter[ $hook ]->callbacks ) ) : 0;
        if ( $count > 15 ) {
            error_log( sprintf( 'هشدار: هوک %s دارای %d callback است — احتمال تعارض', $hook, $count ) );
        }
    }
}

راهنمای تکمیلی در توابع دیباگ وردپرس، بررسی سازگاری قالب و افزونه، شناسایی افزونه مشکل‌ساز، راهنمای حرفه‌ای هوک‌ها، اصول کدنویسی تمیز و توسعه افزونه از صفر آمده است.

اشتباهات رایج در بررسی هوک

فهرست کوتاه اما گران‌قیمت از اشتباهاتی که در کدهای بازبینی‌شده دیده‌ام:

  • اشتباه گرفتن has_action با did_action: اولی بررسی ثبت، دومی بررسی اجرا. راهنما در هوک‌های وردپرس.
  • نبود بررسی priority در has_action: خروجی has_action عدد priority است، نه فقط بولی. اگر می‌خواهید فقط بدانید ثبت شده یا نه، با !== false چک کنید. راهنما در Priority در هوک‌ها.
  • استفاده از has_action در زمان اشتباه: اگر پیش از plugins_loaded استفاده کنید، ممکن است افزونهٔ هدف هنوز بارگذاری نشده باشد. راهنما در کنترل ترتیب اجرای هوک‌ها.
  • نبود بررسی closure در $wp_filter: closureها در آرایه $wp_filter با نام (closure) نمایش داده می‌شوند و قابل شناسایی دقیق نیستند. راهنما در استفاده درست از هوک‌ها.
  • چاپ $wp_filter در Production: این کار، اطلاعات حساس و حجم زیادی به لاگ اضافه می‌کند. همیشه فقط در محیط توسعه و پشت سد دسترسی ادمین. راهنما در امنیت وردپرس برای مبتدیان.
  • نادیده‌گرفتن نسخهٔ وردپرس: ساختار $wp_filter در وردپرس ۴.۷ به بعد از آرایه به کلاس WP_Hook تغییر کرده است. برای پشتیبانی از نسخه‌های قدیمی، باید هر دو حالت را پوشش دهید. راهنما در استانداردهای کدنویسی وردپرس.

فهرست کامل اشتباهات در اشتباهات رایج هوک‌ها، اشتباهات رایج توسعه وردپرس، اشتباهات رایج کدنویسی وردپرس و دیباگ اکشن و فیلتر آمده است.

الگوی حرفه‌ای: بررسی هوک در یک کلاس کمکی

در پروژه‌های بزرگ، تمام بررسی‌های هوک را در یک کلاس کمکی متمرکز کنید:

class My_Plugin_Hook_Checker {
    /**
     * بررسی می‌کند آیا یک اکشن ثبت شده است یا نه.
     *
     * @param string $hook_name نام هوک.
     * @return bool
     */
    public static function action_exists( $hook_name ) {
        return false !== has_action( $hook_name );
    }

    /**
     * بررسی می‌کند آیا یک فیلتر ثبت شده است یا نه.
     *
     * @param string $hook_name نام هوک.
     * @return bool
     */
    public static function filter_exists( $hook_name ) {
        return false !== has_filter( $hook_name );
    }

    /**
     * بررسی می‌کند آیا یک callback خاص ثبت شده است یا نه.
     *
     * @param string $hook_name نام هوک.
     * @param mixed  $callback  نام تابع یا آرایه متد کلاس.
     * @return bool
     */
    public static function callback_registered( $hook_name, $callback ) {
        return false !== has_action( $hook_name, $callback );
    }

    /**
     * بررسی می‌کند آیا یک اکشن اجرا شده است یا نه.
     *
     * @param string $hook_name نام هوک.
     * @return int تعداد اجرا.
     */
    public static function action_fired( $hook_name ) {
        return did_action( $hook_name );
    }

    /**
     * بازگرداندن تمام callbackهای ثبت‌شده روی یک هوک.
     *
     * @param string $hook_name نام هوک.
     * @return array
     */
    public static function get_callbacks( $hook_name ) {
        global $wp_filter;
        if ( ! isset( $wp_filter[ $hook_name ] ) ) {
            return array();
        }
        return $wp_filter[ $hook_name ]->callbacks ?: array();
    }

    /**
     * شمارش تعداد callbackهای ثبت‌شده روی یک هوک.
     *
     * @param string $hook_name نام هوک.
     * @return int
     */
    public static function count_callbacks( $hook_name ) {
        $callbacks = self::get_callbacks( $hook_name );
        return empty( $callbacks ) ? 0 : array_sum( array_map( 'count', $callbacks ) );
    }
}

مزیت: تمام بررسی‌ها در یک نقطه، API یکدست، و امکان تست. الگوهای مشابه در کدنویسی اختصاصی افزونه، ساختار فایل‌های افزونه استاندارد، توسعه افزونه از صفر، استانداردهای کدنویسی وردپرس و ساختاربندی پروژه وردپرس آمده است.

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

بررسی هوک در ووکامرس: مثال‌های واقعی

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

function myshop_init() {
    // بررسی فعال بودن ووکامرس
    if ( ! class_exists( 'WooCommerce' ) ) {
        return;
    }
    
    // بررسی وجود هوک‌های موردنیاز
    if ( has_action( 'woocommerce_thankyou' ) ) {
        add_action( 'woocommerce_thankyou', 'myshop_send_order_notification' );
    }
    
    if ( has_filter( 'woocommerce_get_price_html' ) ) {
        add_filter( 'woocommerce_get_price_html', 'myshop_format_price' );
    }
    
    // بررسی وجود قالب ووکامرس
    if ( has_action( 'woocommerce_before_single_product' ) ) {
        // قالب، استاندارد ووکامرس را رعایت کرده
    }
}
add_action( 'plugins_loaded', 'myshop_init' );

راهنمای کامل در هوک‌های ووکامرس، مدیریت سفارش‌های ووکامرس، سفارشی‌سازی صفحه محصول، سفارشی‌سازی سبد و تسویه‌حساب، راهنمای کار با ووکامرس و ووکامرس چیست آمده است.

بررسی هوک در قالب و چایلد تم

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

// در functions.php چایلد تم
function my_child_init() {
    // بررسی اینکه والد هوک را ثبت کرده
    if ( has_action( 'mytheme_after_header' ) ) {
        add_action( 'mytheme_after_header', 'my_child_add_top_bar' );
    } else {
        // قالب والد هوک را ندارد؛ از روش دیگری استفاده کن
        add_action( 'wp_body_open', 'my_child_add_top_bar' );
    }
}
add_action( 'after_setup_theme', 'my_child_init' );

راهنمای کامل در قالب چایلد چیست، توسعه با چایلد تم، ساختار فایل‌های قالب استاندارد، توسعه قالب از صفر و هوک‌ها در توسعه قالب آمده است.

جمع‌بندی

بررسی وجود هوک در وردپرس، در پنج ابزار خلاصه می‌شود: has_action و has_filter برای بررسی ثبت شدن، did_action برای بررسی اجرا شدن، current_action و current_filter برای تشخیص هوک جاری، و global $wp_filter برای دیدن تمام callbackهای ثبت‌شده. سه اصل را در پایان تاکید می‌کنم: اول، پیش از نوشتن هر callback، وجود هوک را بررسی کنید. دوم، در دیباگ، از did_action و $wp_filter استفاده کنید تا تصویر کامل داشته باشید. سوم، بررسی‌های پرتکرار را در یک کلاس کمکی متمرکز کنید.

اگر امروز یک کار در این مسیر انجام می‌دهید: در پروژه فعلی خود، یکی از فایل‌های افزونه را باز کنید و ببینید آیا در جایی از callback یا فیلتر وابسته استفاده کرده‌اید که بررسی وجود ندارد. همان یک نقطه، نامزد اصلاح است. اگر تجربه‌ای از یک باگ دارید که با بررسی وجود هوک حل شد — یا از یک تابع کمکی برای بررسی هوک استفاده می‌کنید — در دیدگاه‌ها بنویسید؛ همان گزارش‌های واقعی، این راهنما را برای توسعه‌دهنده بعدی دقیق‌تر می‌کند. 🔍