سه ساعت و نیم اختلاف که گزارش‌های فروش را می‌خورد

در سال ۱۳۹۸، یک سایت فروشگاهی با بیش از ده هزار محصول را برای بهینه‌سازی تحویل گرفتم. مدیر سایت شکایت داشت که گزارش‌های فروش «درست نیستند» — گاهی یک روز را دو بار می‌شمردند، گاهی سفارش ساعت ۲۳:۴۵ را به روز بعد نسبت می‌دادند. سه روز وقت گذاشتم تا ریشه را پیدا کنم: در افزونه‌ای که گزارش می‌ساخت، به‌جای تابع current_time() وردپرس از date() خام PHP استفاده شده بود. سرور روی UTC تنظیم بود و منطقه زمانی سایت روی Asia/Tehran؛ نتیجه، سه ساعت و نیم جابه‌جایی در مرزهای روز. آن پروژه به من یاد داد که کار با تاریخ و زمان، ساده به نظر می‌رسد و پر از تله است: منطقه زمانی، فرمت ذخیره‌سازی، و تفاوت timestamp با رشته تاریخ.

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

وردپرس سه مفهوم جداگانه از زمان دارد

قبل از ورود به توابع، یک تفکیک بنیادین که اکثر باگ‌های تاریخ از نادیده‌گرفتنش می‌آید: وردپرس در سه لایه با زمان کار می‌کند، نه یک لایه. لایه اول، timestamp یونیکس: عددی صحیح که تعداد ثانیه‌های سپری‌شده از اول ژانویه ۱۹۷۰ به وقت UTC را نشان می‌دهد. این عدد، مطلق است و به منطقه زمانی وابسته نیست. لایه دوم، datetime محلی: رشته‌ای به فرمت YYYY-MM-DD HH:MM:SS که در جدول wp_posts و متادیتا ذخیره می‌شود. وردپرس این رشته را در منطقه زمانی محلی سایت ذخیره می‌کند، نه UTC. لایه سوم، منطقه زمانی سایت: تنظیماتی که در «تنظیمات ← همگانی» یا با timezone_string در دیتابیس ذخیره می‌شود و تعیین می‌کند «محلی» یعنی چه. درک این سه لایه، تفاوت بین کد درست و کدی که در سرورِ UTC، سه ساعت و نیم عقب می‌افتد را می‌سازد. این تفکیک، مکمل مباحثی است که در ساختار هسته وردپرس و کار با Options API درباره لایه‌های داده گفته‌ام.

تاریخ در وردپرس، یک عدد نیست؛ یک قرارداد بین سرور، دیتابیس، مرورگر کاربر و تنظیمات سایت است. هر کدام از این چهار طرف، زبان خودش را دارد.

توابع پایه: time، current_time، wp_date و date_i18n

چهار تابع پایه که در ۹۰٪ پروژه‌ها به آن‌ها نیاز پیدا می‌کنید:

// زمان فعلی به‌صورت timestamp یونیکس (UTC)
$now_utc = time();

// زمان فعلی با در نظر گرفتن منطقه زمانی سایت
$now_local = current_time( 'timestamp' );

// تاریخ فعلی به‌صورت رشته MySQL (برای ذخیره در دیتابیس)
$now_mysql = current_time( 'mysql' );

// تاریخ فعلی با فرمت دلخواه
$now_custom = current_time( 'Y-m-d H:i' );

// فرمت‌دهی حرفه‌ای با wp_date (از وردپرس ۵.۳)
$formatted = wp_date( 'j F Y', $now_utc );

// فرمت‌دهی با ترجمه و بومی‌سازی
$localized = date_i18n( 'l، j F Y', $now_utc );

تفاوت‌های ظریف این چهار تابع، منبع اکثر اشتباهات است. time(): خام و مطلق، همیشه UTC. برای محاسبات (تفریق، جمع، مقایسه) بهترین گزینه است. current_time( 'timestamp' ): timestamp یونیکس را با offset منطقه زمانی سایت برمی‌گرداند. اگر سایت شما Asia/Tehran است (UTC+3:30)، این تابع عددی حدوداً ۱۲۶۰۰ ثانیه بزرگ‌تر از time() برمی‌گرداند. توجه کنید که این «timestamp محلی» یک عدد جعلی است — در فرمول‌های واقعی می‌تواند گمراه‌کننده باشد. current_time( 'mysql' ): به‌طور خاص برای ذخیره در دیتابیس طراحی شده؛ اگر می‌خواهید تاریخ ذخیره کنید، همیشه از این استفاده کنید. wp_date(): جایگزین مدرن date_i18n است که منطقه زمانی را صریح می‌گیرد و عملکرد بهتری دارد. راهنمای کامل این توابع در توابع داده‌های نوشته و توابع وردپرس چیست آمده است.

یک نکته از تجربه: در پروژه‌ای، تیم توسعه از current_time( 'timestamp' ) برای محاسبه «چند ساعت پیش» استفاده می‌کرد. چون این timestamp محلی است، تفاضل با یک timestamp UTC، همیشه ۳ ساعت و نیم خطا داشت. رفع اشکال، یک خط تغییر بود؛ پیدا کردنش، دو ساعت. از آن پروژه، در محاسبات همیشه از time() خام استفاده می‌کنم و در نمایش، از wp_date یا date_i18n.

توابع تاریخ نوشته: the_time و مشتقاتش

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

// چاپ تاریخ نوشته با فرمت دلخواه
the_time( 'j F Y' );

// برگرداندن تاریخ به‌صورت رشته
$date = get_the_time( 'Y-m-d' );

// تاریخ آخرین ویرایش
the_modified_time( 'j F Y' );

// timestamp نوشته و ویرایش
$post_ts = get_post_timestamp( $post_id );
$mod_ts  = get_post_timestamp( $post_id, 'modified' );

// رشته تاریخ به سبک MySQL
$mysql_date = get_post_time( 'Y-m-d H:i:s', true, $post_id );

// "۳ روز پیش" به‌صورت خوانا
echo human_time_diff( get_the_time( 'U' ), current_time( 'timestamp' ) );

سه نکته ظریف که در پروژه‌ها زیاد دیده‌ام. یک — پارامتر دوم get_post_time: اگر true بدهید، تاریخ به‌صورت GMT برگردانده می‌شود؛ اگر false (پیش‌فرض)، به‌صورت محلی. اشتباه گرفتن این پارامتر، باعث اختلاف ساعت در نمایش می‌شود. دو — human_time_diff: این تابع، تفاوت دو timestamp را به رشته خوانا («۳ روز»، «۲ ساعت») تبدیل می‌کند. برای «چند لحظه پیش» در فهرست نوشته‌ها، انتخاب استاندارد است. سه — get_post_timestamp: از وردپرس ۵.۳ اضافه شده و تابع تمیزتری برای دریافت timestamp است؛ توصیه می‌کنم به‌جای ترکیب get_the_time( 'U' )، از همین استفاده کنید. فهرست کامل این توابع در توابع داده‌های نوشته آمده؛ روش استفاده در قالب در ساختار فایل‌های قالب استاندارد.

منطقه زمانی: wp_timezone و wp_timezone_string

مدیریت منطقه زمانی در وردپرس، قبل و بعد از نسخه ۵.۳ تفاوت محسوسی داشت. توابع مدرن:

// رشته منطقه زمانی سایت (مثلاً Asia/Tehran)
$tz_string = wp_timezone_string();

// شیء DateTimeZone برای استفاده در DateTime
$tz = wp_timezone();

// یک DateTime با منطقه زمانی سایت
$dt = new DateTime( 'now', wp_timezone() );

// تبدیل بین UTC و محلی
$gmt_date = get_gmt_from_date( '2026-09-16 14:00:00' );
$local_date = get_date_from_gmt( $gmt_date );

در پروژه‌های چندساله، توصیه من استفاده از wp_timezone() است نه get_option( 'timezone_string' ). دلیلش: بعضی سایت‌های قدیمی از gmt_offset استفاده می‌کنند (عددی مثل 3.5 برای تهران) و wp_timezone() هر دو حالت را پوشش می‌دهد. یک تجربه میدانی: در سایتی که از ایران به اروپا منتقل شد و منطقه زمانی از Tehran به Berlin تغییر کرد، توابعی که مستقیماً از get_option( 'timezone_string' ) استفاده می‌کردند، پس از مهاجرت خطا دادند چون مقدار جدید رشته‌ای متفاوت بود؛ آن‌هایی که از wp_timezone() استفاده می‌کردند، بدون تغییر ادامه دادند. اصول کار با Options را در کار با Options API و توابع گزینه‌های سایت آورده‌ام.

منطقه زمانی، یک تنظیم نیست؛ یک قرارداد است. هر تابعی که مستقیم به مقدار خام آن وابسته باشد، در روزی که سایت مهاجرت کند، می‌شکند.

فرمت‌دهی و بومی‌سازی فارسی

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

// تبدیل میلادی به شمسی
function my_theme_gregorian_to_jalali( $gy, $gm, $gd ) {
    $g_d_m = array( 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 );
    $gy2 = ( $gm > 2 ) ? ( $gy + 1 ) : $gy;
    $days = 355666 + ( 365 * $gy ) + ( (int) ( ( $gy2 + 3 ) / 4 ) ) - ( (int) ( ( $gy2 + 99 ) / 100 ) ) + ( (int) ( ( $gy2 + 399 ) / 400 ) ) + $gd + $g_d_m[ $gm - 1 ];
    $jy = -1595 + ( 33 * ( (int) ( $days / 12053 ) ) );
    $days %= 12053;
    $jy += 4 * ( (int) ( $days / 1461 ) );
    $days %= 1461;
    if ( $days > 365 ) {
        $jy += (int) ( ( $days - 1 ) / 365 );
        $days = ( $days - 1 ) % 365;
    }
    if ( $days < 186 ) {
        $jm = 1 + (int) ( $days / 31 );
        $jd = 1 + ( $days % 31 );
    } else {
        $jm = 7 + (int) ( ( $days - 186 ) / 30 );
        $jd = 1 + ( ( $days - 186 ) % 30 );
    }
    return array( $jy, $jm, $jd );
}

البته، توابع آماده‌تر و پایدارتر در قالب‌های حرفه‌ای فارسی استفاده می‌شود. اگر در حال ساخت قالب فارسی هستید، فهرست راهنماهای موجود در آماده‌سازی قالب برای فارسی و تفاوت قالب فارسی و انگلیسی را مرور کنید. یک نکته از تجربه: در سایتی که تاریخ‌ها به شمسی نمایش داده می‌شدند ولی در فیلترها به میلادی ذخیره می‌شدند، کاربران در جستجوی «آذر ۱۴۰۳» هیچ نتیجه‌ای نمی‌گرفتند. راه‌حل: ذخیره در میلادی، نمایش در شمسی، جستجو با تبدیل. این جداسازی، در پروژه‌های چندزبانه حیاتی است و در چگونه از وردپرس چندزبانه استفاده کنیم اصولش را آورده‌ام.

کوئری بر اساس تاریخ: date_query

از وردپرس ۳.۷، پارامتر date_query در WP_Query امکان فیلتر بر اساس تاریخ را به شکل تمیز فراهم می‌کند:

$args = array(
    'post_type'      => 'post',
    'posts_per_page' => 10,
    'date_query'     => array(
        array(
            'after'     => '3 months ago',
            'inclusive' => true,
            'column'    => 'post_date',
        ),
        array(
            'before'    => 'now',
            'inclusive' => true,
        ),
        'relation' => 'AND',
    ),
);
$query = new WP_Query( $args );

پارامترهای کلیدی date_query: after / before: می‌توانند رشته تاریخ ('2026-01-01')، رشته نسبی ('3 months ago') یا timestamp باشند. inclusive: آیا خود تاریخ مرزی هم شامل شود. column: روی کدام ستون اعمال شود (post_date، post_modified، post_date_gmt). relation: AND یا OR. نکته مهم: مقادیر داخل date_query بر اساس منطقه زمانی سایت تفسیر می‌شوند، نه UTC — این دقیقاً همان چیزی است که معمولاً می‌خواهیم. راهنمای کامل کوئری‌ها در کدنویسی کوئری سفارشی و توابع کوئری سفارشی. یک نکته عملکردی: در سایت‌های بزرگ، کوئری‌های تاریخ‌محور روی post_date از ایندکس جدول wp_posts استفاده می‌کنند و معمولاً سریع‌تر از فیلترهای متادیتا هستند؛ اما اگر کوئری شما شامل ترکیب تاریخ و تاکسونومی است، بهتر است نتیجه را با Transients کش کنید. الگوی کش در ترنزینت‌ها در وردپرس.

ذخیره و مقایسه تاریخ در متادیتا

در کار با متاباکس‌ها، گاهی نیاز به ذخیره تاریخ دارید — مثلاً تاریخ رویداد، تاریخ انقضا، تاریخ سفارش. دو الگو وجود دارد: الگوی اول، ذخیره به‌صورت رشته MySQL:

$date_string = '2026-09-16 14:30:00';
update_post_meta( $post_id, '_event_date', $date_string );

// خواندن با تبدیل به timestamp
$ts = strtotime( get_post_meta( $post_id, '_event_date', true ) );

الگوی دوم، ذخیره به‌صورت timestamp:

$ts = strtotime( '2026-09-16 14:30:00' );
update_post_meta( $post_id, '_event_ts', $ts );

// خواندن مستقیم
$ts = (int) get_post_meta( $post_id, '_event_ts', true );

توصیه من در پروژه‌های خودم: timestamp را ذخیره کنید. دلیلش، کوئری‌پذیری است: در meta_query، مقایسه عددی روی timestamp بسیار سریع‌تر و دقیق‌تر از مقایسه رشته‌ای تاریخ است. اگر رشته ذخیره کنید، باید در کوئری type => 'DATETIME' بدهید که گاهی ایندکس‌پذیر نیست. نمونه کوئری بر اساس تاریخ رویداد:

$args = array(
    'post_type'  => 'event',
    'meta_query' => array(
        array(
            'key'     => '_event_ts',
            'value'   => time(),
            'compare' => '>=',
            'type'    => 'NUMERIC',
        ),
    ),
);

راهنمای دقیق متادیتا در توابع متادیتا و کار با User Meta. یک تذکر امنیتی: همیشه قبل از ذخیره، تاریخ را پاک‌سازی و اعتبارسنجی کنید؛ رشته تاریخ کاربر می‌تواند ورودی مخرب باشد. راهنمای کامل در پاک‌سازی داده‌ها و اعتبارسنجی داده‌ها.

زمان‌بندی انتشار و cron

وردپرس از زمان‌بندی انتشار نوشته‌ها پشتیبانی می‌کند: نوشته‌ای با وضعیت future ذخیره می‌شود و در زمان مقرر، به‌طور خودکار منتشر می‌شود. ستون‌های مرتبط در دیتابیس wp_posts: post_date، post_date_gmt، post_status. اگر افزونه اختصاصی می‌نویسید، برای هماهنگی با این سیستم:

$post_id = wp_insert_post( array(
    'post_title'   => 'نوشته زمان‌بندی‌شده',
    'post_status'  => 'future',
    'post_date'    => '2026-10-01 10:00:00',
    'post_date_gmt'=> get_gmt_from_date( '2026-10-01 10:00:00' ),
) );

نکته حیاتی: هم post_date (محلی) و هم post_date_gmt را ست کنید. وردپرس روی post_date_gmt تکیه می‌کند برای تعیین «رسیده یا نه». اگر آن را ندهید، ممکن است نوشته ساعت‌ها دیر یا زود منتشر شود. سیستم زمان‌بندی وردپرس روی wp-cron کار می‌کند که خودش فقط با بازدید کاربر اجرا می‌شود. برای سایت‌های حرفه‌ای، توصیه می‌کنم wp-cron را غیرفعال و یک cron واقعی سروری جایگزین کنید. راهنمای کامل در زمان‌بندی cron در وردپرس و عیب‌یابی cron وردپرس. یک تجربه میدانی: در سایتی که خبرهای فوری را زمان‌بندی می‌کرد، تأخیر انتشار بین ۵ تا ۴۰ دقیقه بود. علت: ترافیک شبانه کم بود و wp-cron دیرتر اجرا می‌شد. راه‌حل: cron سروری هر دقیقه. تأخیر به زیر ۳۰ ثانیه رسید.

اشتباهات رایج در کار با تاریخ و زمان

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

  • استفاده از date() به‌جای current_time(): همان پروژه اول این مقاله. همیشه current_time( 'mysql' ) برای ذخیره و wp_date() برای نمایش.
  • استفاده از current_time( 'timestamp' ) در محاسبات: این timestamp محلی است، نه UTC. برای تفریق، از time() خام استفاده کنید.
  • ذخیره تاریخ به‌صورت رشته سفارشی: همیشه Y-m-d H:i:s یا timestamp. رشته‌های دیگر، کوئری را سخت می‌کنند.
  • نبود post_date_gmt در نوشته زمان‌بندی‌شده: تأخیر انتشار، تا ساعت‌ها.
  • استفاده از get_option( 'timezone_string' ) بدون پشتیبانی gmt_offset: سایت‌های قدیمی خطا می‌دهند.
  • نادیده‌گرفتن locale در date_i18n: نام ماه‌ها به انگلیسی نمایش داده می‌شود.
  • تبدیل شمسی دست‌ساز بدون تست لپ‌سال: در سال‌های کبیسه، تاریخ یک روز جابه‌جا می‌شود.
  • نبود escape در نمایش تاریخ: echo get_the_time() بدون esc_html. راهنما در PHP امن در وردپرس.

سه اشتباه دیگر که در پروژه‌های بزرگ دیده‌ام: نبود تست روی سرور با UTC: کدی که روی لوکال با timezone_string تست شده، ممکن است روی سرورِ UTC بشکند. نادیده‌گرفتن ترتیب در مقایسه: در کوئری‌های date_query با relation => 'OR'، ممکن است نتایج غیرمنتظره ببینید. نبود مستندسازی منطقه زمانی پروژه: در تیم، اگر همه ندانند «محلی» یعنی چه، هر کس تفسیر خودش را می‌کند.

جمع‌بندی

توابع تاریخ و زمان وردپرس، در چهار گروه خلاصه می‌شوند: توابع پایه (time، current_time، wp_date)، توابع تاریخ نوشته (the_time، get_the_time، human_time_diff)، توابع منطقه زمانی (wp_timezone، get_gmt_from_date)، و توابع کوئری (date_query). سه اصل را در پایان تاکید می‌کنم: اول، در محاسبات از time() خام و در نمایش از wp_date() استفاده کنید. دوم، همیشه منطقه زمانی سایت را در نظر بگیرید و از wp_timezone() استفاده کنید، نه مقادیر خام. سوم، برای ذخیره در متادیتا، timestamp را ترجیح دهید.

اگر امروز یک کار در این مسیر انجام می‌دهید: به آخرین افزونه یا قالب خود نگاه کنید و ببینید آیا جایی از date() یا get_option( 'timezone_string' ) مستقیم استفاده شده است. اگر بله، همان یک نقطه، نامزد بازبینی است. اگر تجربه‌ای از یک باگ تاریخ‌محور دارید که با تابع درست حل شد، در دیدگاه‌ها بنویسید — همان گزارش‌های واقعی، این راهنما را برای توسعه‌دهنده بعدی دقیق‌تر می‌کند. ⏰