کوئری سفارشی، قلب انعطاف‌پذیری وردپرس است. هر صفحه‌ای که محتوایی متفاوت از پیش‌فرض نشان می‌دهد — اسلایدر، نمونه‌کار فیلترشده، جدول محصولات، لیست نویسندگان — در پشت صحنه یک کوئری سفارشی اجرا می‌شود. در سال‌ها کار با وردپرس، بخش بزرگی از کندی سایت‌ها را در همین لایه دیده‌ام: کوئری‌هایی که بدون آگاهی از ساختار دیتابیس نوشته شده‌اند و در هر بازدید، بار سنگینی روی MySQL می‌گذارند. این مقاله، کدنویسی کوئری سفارشی در وردپرس را از پایه تا بهینه‌سازی مرور می‌کند. برای درک پیش‌نیازها، افزونه وردپرس چیست، توابع وردپرس برای کوئری سفارشی، و کار با CPT را پیش از ادامه ببینید.

کوئری سفارشی چیست و چه زمانی لازم است؟

در وردپرس، «حلقهٔ اصلی» برای هر نوع صفحه به‌طور خودکار ساخته می‌شود: در صفحهٔ خانه، آخرین نوشته‌ها؛ در آرشیو دسته، نوشته‌های آن دسته؛ در صفحهٔ جستجو، نتایج مرتبط. کوئری سفارشی زمانی لازم می‌شود که می‌خواهید محتوایی متفاوت از این پیش‌فرض‌ها نمایش دهید. پنج سناریوی پرتکرار: یک — بخش‌های ویژهٔ صفحهٔ اصلی. مثال: آخرین پروژه‌های منتخب، نوشته‌های پربازدید. دو — لیست‌های فیلترشده. مثال: محصولات یک برند خاص، پروژه‌های سال ۱۴۰۳. سه — روابط بین محتواها. مثال: نوشته‌های مرتبط با یک محصول. چهار — داده‌های آماری. مثال: محتوای محبوب بر اساس شمارش بازدید. پنج — ترکیب چند معیار. مثال: پروژه‌های دستهٔ خاص با فیلد «سال اجرا» بزرگتر از ۲۰۲۳. سه لایهٔ کوئری سفارشی را در ادامه باز می‌کنم: WP_Query (سطح بالا)، get_posts (میان‌رده)، و wpdb (سطح پایین).

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

WP_Query: قلب کوئری سفارشی

WP_Query، کلاس اصلی وردپرس برای پرس‌وجوی محتواست. هر کوئری سفارشی، در نهایت به این کلاس می‌رسد. اسکلت پایه:

$args = array(
    'post_type'      => 'post',
    'posts_per_page' => 10,
    'orderby'        => 'date',
    'order'          => 'DESC',
);
$query = new WP_Query( $args );

if ( $query->have_posts() ) :
    while ( $query->have_posts() ) : $query->the_post();
        ?>
        <article>
            <h2><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
            <?php the_excerpt(); ?>
        </article>
        <?php
    endwhile;
    wp_reset_postdata();
endif;

نکتهٔ حیاتی: پس از حلقهٔ WP_Query سفارشی، حتماً wp_reset_postdata() را فراخوانی کنید. بدون این، متغیر جهانی $post به آخرین نوشتهٔ کوئری سفارشی اشاره می‌کند و حلقهٔ بعدی دچار مشکل می‌شود. راهنمای کامل در توابع کوئری سفارشی و توابع داده‌های نوشته.

پارامترهای کلیدی WP_Query

پارامترهای پرکاربرد WP_Query در پنج دسته:

دستهپارامترکاربرد
نوعpost_typeنوع محتوا (post، page، CPT سفارشی)
تعدادposts_per_pageتعداد نوشته در هر صفحه
ترتیبorderbyمعیار ترتیب (date، title، menu_order، rand، meta_value)
وضعیتpost_statuspublish، draft، private، any
وابستگیpost__inلیست مشخصی از IDها

مثال ترکیبی:

$args = array(
    'post_type'      => array( 'post', 'article' ),
    'posts_per_page' => 6,
    'orderby'        => array(
        'menu_order' => 'ASC',
        'date'       => 'DESC',
    ),
    'post_status'    => 'publish',
    'post__in'       => array( 12, 34, 56, 78 ),
);
$query = new WP_Query( $args );

نکته: post__in با post__not_in ترکیب می‌شود. توجه: پارامتر post__in وقتی با ترتیب‌دهی عادی استفاده شود، ترتیب لیست ورودی را حفظ نمی‌کند. برای حفظ ترتیب، از orderby => 'post__in' استفاده کنید.

کوئری بر اساس متادیتا

برای فیلتر محتوا بر اساس متادیتا، از پارامتر meta_query استفاده کنید:

$args = array(
    'post_type'      => 'project',
    'posts_per_page' => 12,
    'meta_query'     => array(
        'relation' => 'AND',
        array(
            'key'     => '_project_year',
            'value'   => 2023,
            'compare' => '>=',
            'type'    => 'NUMERIC',
        ),
        array(
            'key'     => '_project_status',
            'value'   => 'completed',
            'compare' => '=',
        ),
    ),
);

پارامترهای مهم در meta_query: key: نام متادیتا. value: مقدار مقایسه. compare: عملگر مقایسه (=، !=، >، <، >=، <=، LIKE، NOT LIKE، IN، NOT IN، BETWEEN، NOT BETWEEN، EXISTS، NOT EXISTS). type: نوع داده (NUMERIC، BINARY، CHAR، DATE، DATETIME، DECIMAL، SIGNED، TIME، UNSIGNED). نکته: نوع داده را درست انتخاب کنید. اگر متادیتا عددی است و type را NUMERIC بگذارید، مقایسه عددی صحیح انجام می‌شود؛ در غیر این صورت، مقایسه رشته‌ای که در اعداد چند رقمی نتایج اشتباه می‌دهد. راهنمای متادیتا در کار با متاباکس‌ها و توابع متادیتا.

برای کوئری‌های پیچیده‌تر، meta_query تودرتو:

'meta_query' => array(
    'relation' => 'OR',
    array(
        'relation' => 'AND',
        array( 'key' => '_price', 'value' => 1000000, 'compare' => '>', 'type' => 'NUMERIC' ),
        array( 'key' => '_in_stock', 'value' => '1' ),
    ),
    array(
        'key'     => '_featured',
        'value'   => '1',
        'compare' => '=',
    ),
),

کوئری بر اساس تاکسونومی

فیلتر بر اساس تاکسونومی، با پارامتر tax_query:

$args = array(
    'post_type'      => 'product',
    'posts_per_page' => 12,
    'tax_query'      => array(
        'relation' => 'AND',
        array(
            'taxonomy' => 'brand',
            'field'    => 'slug',
            'terms'    => 'apple',
        ),
        array(
            'taxonomy' => 'product_cat',
            'field'    => 'term_id',
            'terms'    => array( 5, 12 ),
            'operator' => 'IN',
        ),
    ),
);

پارامترهای مهم در tax_query: taxonomy: نام تاکسونومی. field: نوع مقدار (term_id، slug، name، term_taxonomy_id). terms: مقدار یا آرایهٔ مقادیر. operator: عملگر (IN، NOT IN، AND، EXISTS، NOT EXISTS). include_children: شامل فرزندان در تاکسونومی سلسله‌مراتبی. راهنمای کامل در کار با تاکسونومی سفارشی، توابع دسته‌بندی، و توابع برچسب.

حلقه و بازگردانی داده

پس از اجرای کوئری، حلقهٔ وردپرس کار می‌کند. دسترسی به داده‌های داخل حلقه:

if ( $query->have_posts() ) :
    while ( $query->have_posts() ) : $query->the_post();
        $post_id    = get_the_ID();
        $title      = get_the_title();
        $permalink  = get_permalink();
        $thumb      = get_the_post_thumbnail_url( $post_id, 'medium' );
        $year       = get_post_meta( $post_id, '_project_year', true );
        $terms      = get_the_terms( $post_id, 'project_cat' );
        ?>
        <article class="project-card">
            <?php if ( $thumb ) : ?>
                <img src="<?php echo esc_url( $thumb ); ?>" alt="<?php echo esc_attr( $title ); ?>" />
            <?php endif; ?>
            <h3><a href="<?php echo esc_url( $permalink ); ?>"><?php echo esc_html( $title ); ?></a></h3>
            <?php if ( $year ) : ?>
                <p>سال اجرا: <?php echo esc_html( $year ); ?></p>
            <?php endif; ?>
        </article>
        <?php
    endwhile;
    wp_reset_postdata();
endif;

سه نکته: یک — escape خروجی: esc_url، esc_html، esc_attr. دو — بررسی خالی بودن: پیش از استفاده از متادیتا یا ترم، خالی نبودن را بررسی کنید. سه — is_wp_error: پس از get_the_terms، همیشه بررسی کنید. راهنما در PHP امن در وردپرس و پاک‌سازی داده‌ها.

صفحه‌بندی در کوئری سفارشی

صفحه‌بندی در WP_Query، با پارامتر paged:

$paged = max( 1, get_query_var( 'paged' ) );

$args = array(
    'post_type'      => 'project',
    'posts_per_page' => 12,
    'paged'          => $paged,
);
$query = new WP_Query( $args );

// پس از حلقه
$pagination = paginate_links( array(
    'total'     => $query->max_num_pages,
    'current'   = $paged,
    'prev_text' = 'قبلی',
    'next_text' = 'بعدی',
) );

if ( $pagination ) {
    echo '<nav class="pagination">' . $pagination . '</nav>';
}

نکته: برای صفحه‌بندی کارآمد، نباید no_found_rows => true بگذارید؛ چون این پارامتر شمارش کل را حذف می‌کند و max_num_pages صفر می‌شود. اما اگر صفحه‌بندی ندارید، این پارامتر را برای بهینه‌سازی اضافه کنید. الگوهای صفحه‌بندی در بهینه‌سازی کد وردپرس و ساختار URL و سئو.

کوئری‌های تودرتو

گاهی نیاز به اجرای کوئری درون حلقهٔ کوئری دیگر است — مثلاً نمایش نوشته‌های مرتبط برای هر پروژه. الگوی نادرست:

// نامناسب
while ( $query->have_posts() ) :
    $query->the_post();
    $related = new WP_Query( array( 'post__in' => get_related_ids() ) );
    // حلقه تودرتو
endwhile;

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

// ابتدا همهٔ IDهای موردنیاز را جمع کنید
$related_ids = array();
while ( $query->have_posts() ) :
    $query->the_post();
    $related_ids = array_merge( $related_ids, get_related_ids( get_the_ID() ) );
endwhile;

// سپس یک کوئری واحد
$related_query = new WP_Query( array(
    'post__in'       => array_unique( $related_ids ),
    'posts_per_page' => -1,
) );

// نگهداری در آرایهٔ hash برای دسترسی سریع
$related_map = array();
while ( $related_query->have_posts() ) :
    $related_query->the_post();
    $related_map[ get_the_ID() ] = get_the_title();
endwhile;

این الگو، تعداد کوئری‌ها را از N به ۱ کاهش می‌دهد. راهنمای کامل در بهینه‌سازی کوئری‌ها و بهینه‌سازی کد وردپرس.

pre_get_posts و تغییر حلقهٔ اصلی

برای تغییر حلقهٔ اصلی وردپرس (بدون ساخت کوئری جدید)، از هوک pre_get_posts استفاده کنید. الگوی کلاسیک: اضافه‌کردن CPT به آرشیو جستجو:

function my_plugin_include_cpt_in_search( $query ) {
    if ( is_admin() || ! $query->is_main_query() ) {
        return;
    }
    if ( $query->is_search() ) {
        $query->set( 'post_type', array( 'post', 'page', 'project', 'product' ) );
    }
}
add_action( 'pre_get_posts', 'my_plugin_include_cpt_in_search' );

نکته: دو بررسی الزامی: یک — is_admin(): تا تغییرات فقط روی front-end اعمال شود. دو — is_main_query(): تا حلقه‌های جانبی تغییر نکنند. الگوهای بیشتر در هوک‌های وردپرس، نحوهٔ استفاده از add_action، و راهنمای حرفه‌ای هوک‌ها.

کوئری خام با wpdb

در مواردی که WP_Query جواب نمی‌دهد — مثل کوئری‌های آماری یا عملیات پیچیده روی جداول اختصاصی — از $wpdb استفاده کنید. الگوی امن:

global $wpdb;

$author_id = 5;
$status    = 'publish';

$sql = $wpdb->prepare(
    "SELECT ID, post_title
     FROM {$wpdb->posts}
     WHERE post_author = %d
       AND post_status = %s
     ORDER BY post_date DESC
     LIMIT 20",
    $author_id,
    $status
);

$results = $wpdb->get_results( $sql );

foreach ( $results as $row ) {
    printf(
        '<li>%s</li>',
        esc_html( $row->post_title )
    );
}

نکتهٔ حیاتی: $wpdb->prepare الزامی است. بدون آن، خطر SQL Injection جدی. سه قاعده: یک — placeholder درست: %d برای عدد، %s برای رشته، %f برای اعشار. دو — نام جدول با {$wpdb->prefix}: تا در سایت‌های با پیشوند سفارشی کار کند. سه — escape خروجی: نتایج را با esc_html نمایش دهید. راهنمای کامل در PHP امن در وردپرس، پاک‌سازی داده‌ها، و بهینه‌سازی کوئری‌های MySQL.

بهینه‌سازی کوئری‌ها

پنج تکنیک بهینه‌سازی در کوئری سفارشی: یک — محدودسازی posts_per_page: هرگز -1 بدون دلیل. دو — no_found_rows => true: اگر صفحه‌بندی ندارید، کوئری شمارش را حذف می‌کند. سه — update_post_meta_cache => false: اگر از متا استفاده نمی‌کنید. چهار — update_post_term_cache => false: اگر از ترم استفاده نمی‌کنید. پنج — fields => 'ids': اگر فقط ID لازم است.

$args = array(
    'post_type'              => 'project',
    'posts_per_page'         => 12,
    'no_found_rows'          => true,
    'update_post_meta_cache' => false,
    'update_post_term_cache' => false,
);

روش سنجش با Query Monitor: در صفحه‌ای که کوئری سفارشی دارد، این افزونه تعداد، زمان، و منبع هر کوئری را نشان می‌دهد. راهنمای کامل در بهینه‌سازی کوئری‌ها، بهینه‌سازی کد وردپرس، و ابزارهای تست سرعت سایت. یک نکتهٔ میدانی: در سایت‌های با بیش از ۱۰ هزار نوشته، کوئری‌های مبتنی بر meta_query سنگین می‌شوند. اگر طرح پروژه اجازه می‌دهد، به‌جای متادیتا، از تاکسونومی برای فیلتر استفاده کنید — ایندکس‌گذاری در جدول wp_term_relationships بهتر از wp_postmeta است. راهنمای تفصیلی در کار با تاکسونومی سفارشی.

کش نتایج

کوئری‌های سنگین را با Transients کش کنید:

function my_plugin_get_featured_projects() {
    $cache_key = 'my_featured_projects';
    $cached = get_transient( $cache_key );

    if ( $cached !== false ) {
        return $cached;
    }

    $query = new WP_Query( array(
        'post_type'      => 'project',
        'posts_per_page' => 6,
        'meta_key'       => '_featured',
        'meta_value'     => '1',
    ) );

    $data = array();
    while ( $query->have_posts() ) :
        $query->the_post();
        $data[] = array(
            'id'    => get_the_ID(),
            'title' => get_the_title(),
            'url'   => get_permalink(),
            'thumb' = get_the_post_thumbnail_url( get_the_ID(), 'medium' ),
        );
    endwhile;
    wp_reset_postdata();

    set_transient( $cache_key, $data, HOUR_IN_SECONDS );
    return $data;
}

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

add_action( 'save_post_project', function() {
    delete_transient( 'my_featured_projects' );
} );

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

ساختار کلاس‌محور

در پروژه‌های جدی، کوئری سفارشی در یک کلاس اختصاصی قرار می‌گیرد:

class My_Plugin_Project_Query {

    public static function get_featured( $limit = 6 ) {
        $cache_key = 'my_featured_projects_' . $limit;
        $cached = get_transient( $cache_key );
        if ( $cached !== false ) {
            return $cached;
        }

        $query = new WP_Query( array(
            'post_type'              => 'project',
            'posts_per_page'         => $limit,
            'meta_key'               => '_featured',
            'meta_value'             => '1',
            'no_found_rows'          => true,
            'update_post_meta_cache' => false,
            'update_post_term_cache' => false,
        ) );

        $data = array();
        while ( $query->have_posts() ) :
            $query->the_post();
            $data[] = self::format_item( get_post() );
        endwhile;
        wp_reset_postdata();

        set_transient( $cache_key, $data, HOUR_IN_SECONDS );
        return $data;
    }

    public static function get_by_taxonomy( $taxonomy, $term, $limit = 12 ) {
        $query = new WP_Query( array(
            'post_type'      => 'project',
            'posts_per_page' => $limit,
            'tax_query'      => array(
                array(
                    'taxonomy' => $taxonomy,
                    'field'    => 'slug',
                    'terms'    => $term,
                ),
            ),
        ) );

        $data = array();
        while ( $query->have_posts() ) :
            $query->the_post();
            $data[] = self::format_item( get_post() );
        endwhile;
        wp_reset_postdata();

        return $data;
    }

    private static function format_item( $post ) {
        return array(
            'id'    => $post->ID,
            'title' => get_the_title( $post ),
            'url'   => get_permalink( $post ),
            'thumb' = get_the_post_thumbnail_url( $post, 'medium' ),
            'year'  => get_post_meta( $post->ID, '_project_year', true ),
        );
    }
}
My_Plugin_Project_Query::init();

مزیت این ساختار: تمام کوئری‌ها در یک نقطه، مدیریت کش متمرکز، و شکل خروجی یکسان. الگوهای مشابه در کدنویسی اختصاصی افزونه، ساختار فایل‌های افزونهٔ استاندارد، و استانداردهای کدنویسی وردپرس. این جداسازی، در پروژه‌های بزرگ، تفاوت بین کدی که ماه‌ها بعد قابل نگهداری است و کدی که به بدهی فنی تبدیل می‌شود را می‌سازد. یک نکتهٔ تکمیلی: در پروژه‌ای با سه کوئری سنگین که در یک صفحه اجرا می‌شدند، تجمیع آن‌ها در یک کلاس با کش متمرکز، زمان پاسخ را از ۲.۸ ثانیه به ۱.۲ ثانیه کاهش داد.

اشتباهات رایج

کدنویسی کوئری سفارشی در وردپرس، مسیر روشنی دارد: چهار لایهٔ کوئری (WP_Query، get_posts، pre_get_posts، wpdb)، پارامترهای درست برای هر نوع فیلتر (متادیتا، تاکسونومی، نوع)، حلقه و escape خروجی، صفحه‌بندی، اجتناب از کوئری‌های تودرتو، بهینه‌سازی با پارامترهای سبک‌ساز، کش نتایج، و ساختار کلاس‌محور. اگر امروز یک کار در این مسیر انجام می‌دهید: با Query Monitor به صفحه‌ای که کوئری سفارشی دارد نگاه کنید و ببینید کدام کوئری بیشترین زمان را می‌گیرد؛ همان یک عدد، نقشهٔ بهبود شماست. اگر تجربه‌ای از یک کوئری سنگین در پروژه‌ای دارید که با کش یا بازنویسی سبک شد، در دیدگاه‌ها بنویسید؛ همان گزارش‌های واقعی، این راهنما را دقیق‌تر می‌کند. 🔍