در حال بارگذاری 0%
// منو.exe
خانه رزومه بلاگ تماس سفارش
English فارسی
~/blog / programming / year-2038-problem-php-unix-timestamp

سال ۲۰۳۸: وقتی تایم‌استمپ یونیکس در PHP از کار می‌افته

فرض کنید برای یک سامانه اشتراک، بیمه یا رزرو طولانی‌مدت باید تاریخ اول ژانویه ۲۰۴۰ را ذخیره کنیم. کد بسیار ساده به نظر می‌رسد — اما همین امروز، فقط به‌دلیل کارکردن با یک تاریخ آینده، برنامه می‌تواند با سرریز Unix Timestamp روبه‌رو شود. در این مقاله مشکل Year 2038 را از ریشه عددی آن تا لایه دیتابیس عملی بررسی می‌کنیم.

سال ۲۰۳۸: وقتی تایم‌استمپ یونیکس در PHP از کار می‌افته

# یک کد ساده که روی نیمی از سیستم‌ها می‌شکند

expires.php
 
$expiresAt = strtotime('2040-01-01 00:00:00 UTC');
var_dump($expiresAt);
# روی PHP 64-bit:
int(2208988800)
اما همین کد روی یک اجرای ۳۲‌بیتی ممکن است false برگرداند، با خطای خارج‌بودن تاریخ از محدوده روبه‌رو شود یا در لایه دیگری مقدار نادرستی تولید کند. جالب‌تر اینکه هنوز به سال ۲۰۳۸ نرسیده‌ایم؛ برنامه همین امروز، فقط به‌دلیل کارکردن با یک تاریخ آینده، می‌تواند با این مشکل برخورد کند.
چطور ممکن است یک کامپیوتر سال ۲۰۴۰ را نشناسد؟ مشکل از تقویم نیست؛ مشکل از عددی است که پشت تاریخ پنهان شده است.

# اول ببینیم Unix Timestamp دقیقاً چیست

بسیاری از سیستم‌ها زمان را به‌صورت یک تاریخ خوانا مانند 2040-01-01 نگه نمی‌دارند. آن‌ها تعداد ثانیه‌های گذشته از لحظه‌ای ثابت را ذخیره می‌کنند:
EPOCH
Unix Epoch = 1970-01-01 00:00:00 UTC = 0
برای نمونه:
timestamps.txt
1970-01-01 00:00:00 UTC →           0
2000-01-01 00:00:00 UTC →    946684800
2038-01-19 03:14:07 UTC → 2147483647
2040-01-01 00:00:00 UTC →   2208988800
در PHP تابع time() همین تعداد ثانیه را برای لحظه فعلی برمی‌گرداند و توابعی مانند strtotime() و متد getTimestamp() نیز در نهایت یک Unix Timestamp تولید می‌کنند. خود Timestamp اطلاعات منطقه زمانی را نگه نمی‌دارد؛ فقط یک لحظه روی خط زمان است. منطقه زمانی هنگام تبدیل این عدد به تاریخ محلی وارد محاسبه می‌شود.
تا اینجا همه‌چیز منطقی است. مشکل وقتی آغاز می‌شود که بخواهیم این شمارنده را در یک عدد صحیح علامت‌دار ۳۲‌بیتی قرار دهیم.

# چرا عدد ۲۱۴۷۴۸۳۶۴۷ مرز ماجراست؟

یک عدد صحیح ۳۲‌بیتی دقیقاً ۳۲ خانه دودویی دارد. در نمایش علامت‌دار رایج، بخشی از محدوده برای اعداد منفی استفاده می‌شود:
INT32 SIGNED
−231 = −2147483648  |  231 − 1 = 2147483647
کمترین | بیشترین
اگر این عدد را تعداد ثانیه‌های گذشته از سال ۱۹۷۰ در نظر بگیریم، بیشترین مقدار مثبت دقیقاً به این لحظه می‌رسد:
THE MOMENT
2147483647 = 2038-01-19 03:14:07 UTC
یک ثانیه بعد، مقدار موردنیاز 2147483648 است؛ اما این عدد دیگر داخل int32 signed جا نمی‌شود. در شکل کلاسیک سرریز، بیت‌ها دوباره از ابتدای محدوده منفی ادامه پیدا می‌کنند:
سرریز کلاسیک
2147483647
2038-01-19 03:14:07
۱ ثانیه
−2147483648
1901-12-13 20:45:52
یعنی سیستمی که باید «یک ثانیه جلوتر» برود، ناگهان بیش از ۱۳۶ سال به عقب برمی‌گردد. البته همه سیستم‌ها دقیقاً این رفتار را ندارند؛ بعضی مقدار false می‌دهند، بعضی خطا ایجاد می‌کنند و بعضی تاریخ را نامعتبر تشخیص می‌دهند. وجه مشترک همه آن‌ها این است که مقدار جدید در قالب ۳۲‌بیتی قابل نمایش نیست.

# سرریز ۳۲‌بیتی را با PHP شبیه‌سازی کنیم

کد زیر باید روی PHP ۶۴‌بیتی اجرا شود. ما عمداً رفتار یک عدد صحیح ۳۲‌بیتی را شبیه‌سازی می‌کنیم؛ بنابراین اجرای PHP واقعی ما دچار سرریز نشده است:
overflow_sim.php
 
date_default_timezone_set('UTC');
 
function asSignedInt32(int $value): int
{
    $fullRange = 4294967296; // 2^32
    $signPoint = 2147483648; // 2^31
 
    $wrapped = $value % $fullRange;
 
    return $wrapped >= $signPoint
        ? $wrapped - $fullRange
        : $wrapped;
}
 
$lastValid = 2147483647;
$nextSecond = $lastValid + 1;
$wrappedValue = asSignedInt32($nextSecond);
 
echo gmdate('Y-m-d H:i:s', $lastValid), " UTC\n";
echo gmdate('Y-m-d H:i:s', $nextSecond), " UTC\n";
echo "32-bit value: {$wrappedValue}\n";
echo gmdate('Y-m-d H:i:s', $wrappedValue), " UTC\n";
2038-01-19 03:14:07 UTC
2038-01-19 03:14:08 UTC ← زمان درست (int64)
32-bit value: -2147483648
1901-12-13 20:45:52 UTC ← نتیجه سرریز
خط دوم زمان درست را نشان می‌دهد؛ چیزی که یک عدد ۶۴‌بیتی به‌راحتی نگه می‌دارد. دو خط بعدی نشان می‌دهند اگر همان مقدار به قالب علامت‌دار ۳۲‌بیتی محدود شود، چه اتفاقی می‌افتد.

# آیا مشکل سال ۲۰۳۸ در PHP حل شده است؟

جواب دقیق این است: در PHP یک نسخه جادویی وجود ندارد که مشکل را روی تمام سیستم‌ها حل کرده باشد. اندازه نوع int در PHP به پلتفرم و نوع Build بستگی دارد. مستندات PHP می‌گویند در اجرای ۳۲‌بیتی بیشترین مقدار معمولاً حدود دو میلیارد و در اجرای ۶۴‌بیتی حدود 9.22 × 10^18 است. پس یک PHP جدید نیز اگر به‌صورت ۳۲‌بیتی ساخته و اجرا شده باشد، برای Unix Timestamp همچنان به مرز ۲۰۳۸ می‌رسد.
بااین‌حال اگر بخواهیم یک نسخه مهم را نام ببریم، PHP 7.0 نقطه عطف است. تیم PHP در اعلان رسمی این نسخه، «پشتیبانی یکدست ۶۴‌بیتی» یا Consistent 64-bit support را یکی از قابلیت‌های PHP 7 معرفی کرد. در نتیجه PHP 7 و PHP 8 روی یک Build واقعی ۶۴‌بیتی می‌توانند Timestampهای بعد از ۲۰۳۸ را به‌صورت int نگه دارند. اما این جمله دو شرط دارد:
عبور از مرز ۲۰۳۸
نسخه مناسب PHP
اجرای واقعی 64-bit
عبور int از مرز 2038
صرف اینکه سیستم‌عامل ۶۴‌بیتی باشد کافی نیست؛ ممکن است Binary یا Container مربوط به PHP هنوز ۳۲‌بیتی باشد. از طرف دیگر، بعضی نسخه‌های قدیمی PHP روی پلتفرم‌های ۶۴‌بیتی نیز قبلاً int ۶۴‌بیتی داشتند. به همین دلیل شماره نسخه به‌تنهایی پاسخ قطعی نمی‌دهد:
۳۲ب
PHP قدیمی روی Build ۳۲‌بیتی
در معرض محدودیت ۲۰۳۸
؟
PHP قدیمی روی بعضی پلتفرم‌های ۶۴‌بیتی
ممکن است Timestamp ۶۴‌بیتی داشته باشد؛ باید بررسی شود
OK
PHP 7.0+ روی Build ۶۴‌بیتی
نوع int برای تاریخ‌های بعد از ۲۰۳۸ کافی است
۳۲ب
PHP 7 یا PHP 8 روی Build ۳۲‌بیتی
محدودیت int ۳۲‌بیتی همچنان باقی است
پاسخ کوتاه: PHP 7.0 پشتیبانی ۶۴‌بیتی را یکدست کرد، اما حل‌شدن واقعی مشکل به ۶۴‌بیتی بودن اجرای PHP و تمام لایه‌های بعدی سیستم وابسته است. PHP 7 را هم نباید صرفاً برای حل این مشکل نصب کرد؛ این شاخه قدیمی است. در پروژه واقعی باید از یک نسخه پشتیبانی‌شده PHP با Build ۶۴‌بیتی استفاده شود.

# چطور بفهمیم PHP سرور ۳۲‌بیتی است یا ۶۴‌بیتی؟

مطمئن‌ترین بررسی داخل خود PHP انجام می‌شود:
check_bits.php
 
echo 'PHP version: ', PHP_VERSION, PHP_EOL;
echo 'Integer size: ', PHP_INT_SIZE, ' bytes', PHP_EOL;
echo 'Integer bits: ', PHP_INT_SIZE * 8, PHP_EOL;
echo 'PHP_INT_MAX: ', PHP_INT_MAX, PHP_EOL;
 
if (PHP_INT_SIZE >= 8 && PHP_INT_MAX > 2147483647) {
    echo "Unix Timestamp as int can pass the 2038 boundary.\n";
} else {
    echo "Warning: 32-bit integer range detected.\n";
}
# PHP 64-bit:
Integer size: 8 bytes → bits: 64 → PHP_INT_MAX: 9223372036854775807
# PHP 32-bit:
Integer size: 4 bytes → bits: 32 → PHP_INT_MAX: 2147483647
معیار اصلی PHP_INT_SIZE است: مقدار 8 یعنی هر عدد صحیح ۶۴ بیت دارد و مقدار 4 یعنی با یک اجرای ۳۲‌بیتی روبه‌رو هستیم.
این تست را باید در همان محیطی اجرا کنید که برنامه واقعاً در آن کار می‌کند: همان Container، همان PHP-FPM، همان Worker صف و همان سرور Cron. خروجی PHP خط فرمان لزوماً با PHP متصل به وب‌سرور یکسان نیست.

# یک آزمایش واقعی با تاریخ‌های مرزی

کد زیر سه لحظه مهم را بررسی می‌کند:
boundary_test.php
 
$dates = [
    '2038-01-19 03:14:07 UTC',
    '2038-01-19 03:14:08 UTC',
    '2040-01-01 00:00:00 UTC',
];
 
foreach ($dates as $input) {
    $date = new DateTimeImmutable($input);
 
    echo $date->format('Y-m-d H:i:s T'), PHP_EOL;
    echo 'format("U"): ', $date->format('U'), PHP_EOL;
 
    try {
        echo 'getTimestamp(): ', $date->getTimestamp(), PHP_EOL;
    } catch (Throwable $error) {
        echo get_class($error), ': ', $error->getMessage(), PHP_EOL;
    }
 
    echo "---\n";
}
روی PHP ۶۴‌بیتی، بخش‌های اصلی خروجی چنین خواهند بود:
output — 64-bit
2038-01-19 03:14:07 UTC → format("U"): 2147483647 | getTimestamp(): 2147483647
2038-01-19 03:14:08 UTC → format("U"): 2147483648 | getTimestamp(): 2147483648
2040-01-01 00:00:00 UTC → format("U"): 2208988800 | getTimestamp(): 2208988800
تفاوت مهمی میان format('U') و getTimestamp() وجود دارد. متد getTimestamp() باید یک int برگرداند؛ بنابراین اگر مقدار داخل int پلتفرم جا نشود، شکست می‌خورد:
<8
قبل از PHP 8.0
getTimestamp() برای مقدار خارج از محدوده، false برمی‌گرداند
8.0
PHP 8.0 تا 8.2
خطای ValueError ایجاد می‌کند
8.3
PHP 8.3 و جدیدتر
خطای مشخص‌تر DateRangeError ایجاد می‌کند
در مقابل، format('U') مقدار Timestamp را به‌صورت رشته برمی‌گرداند و مستندات PHP نیز همین روش را برای زمانی پیشنهاد می‌کنند که Timestamp داخل int قابل نمایش نیست. این قابلیت برای خواندن یا انتقال مقدار مفید است، اما به معنی امن‌شدن تمام عملیات عددی نیست؛ اگر رشته "2208988800" را دوباره روی یک PHP ۳۲‌بیتی به int تبدیل کنیم، همان محدودیت بازمی‌گردد.

# آیا استفاده از DateTimeImmutable به‌تنهایی مشکل را حل می‌کند؟

DateTimeImmutable برای مدیریت تاریخ انتخاب بهتری از جمع‌وتفریق دستی Timestampهاست. این کلاس منطقه زمانی، سال کبیسه، تعداد متفاوت روزهای ماه و تغییرات ساعت رسمی را بهتر مدیریت می‌کند و مستندات PHP نیز برای محاسبات زمانی استفاده از آن را به‌جای عملیات ریاضی روی strtotime() توصیه می‌کنند.
طراحی بد

جمع ثانیه‌ای — دقیقاً معادل ۱۵ سال تقویمی نیست و روی سیستم ۳۲‌بیتی ممکن است زودتر از ۲۰۳۸ سرریز شود.

$expiresAt = time() + (15 * 365 * 24 * 60 * 60);
طراحی بهتر

محاسبه تقویمی با DateInterval.

$expiresAt = $now->add(new DateInterval('P15Y'));
اما حتی در این طراحی نیز اگر در مرحله بعد getTimestamp() را روی PHP ۳۲‌بیتی صدا بزنیم یا تاریخ را داخل ستون ۳۲‌بیتی دیتابیس بریزیم، مشکل دوباره ظاهر می‌شود. DateTimeImmutable محاسبه تقویمی را بهتر می‌کند؛ محدودیت نوع داده در لایه‌های دیگر را حذف نمی‌کند.

# حتی اگر PHP امن باشد، MySQL ممکن است جلوی شما را بگیرد

فرض کنید PHP ما ۶۴‌بیتی است و مقدار 2208988800 را بدون مشکل تولید می‌کند. حالا یک جدول MySQL داریم:
schema_bad.sql
CREATE TABLE subscriptions_bad (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    expires_at TIMESTAMP NOT NULL
);
در MySQL 8.4 نیز نوع TIMESTAMP فقط بازه زیر را پشتیبانی می‌کند:
TIMESTAMP range
1970-01-01 00:00:01 UTC
        تا
2038-01-19 03:14:07 UTC
در حالت Strict، تاریخ ۲۰۴۰ با خطای خارج‌بودن مقدار از محدوده رد می‌شود. یعنی ارتقای PHP به‌تنهایی کافی نیست. برای تاریخ‌های تقویمی آینده می‌توان از DATETIME استفاده کرد:
schema_good.sql
CREATE TABLE subscriptions (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    expires_at_utc DATETIME(6) NOT NULL
);
بازه DATETIME در MySQL از سال ۱۰۰۰ تا ۹۹۹۹ است. برخلاف TIMESTAMP، مقدار DATETIME به‌صورت خودکار با منطقه زمانی Session به UTC تبدیل و دوباره برگردانده نمی‌شود. به همین دلیل نام ستون را expires_at_utc گذاشتیم و برنامه باید پیش از ذخیره، مقدار را صریحاً به UTC تبدیل کند:
insert.php
 
$expiresAt = new DateTimeImmutable(
    '2040-01-01 00:00:00',
    new DateTimeZone('UTC')
);
 
$statement = $pdo->prepare(
    'INSERT INTO subscriptions (expires_at_utc) VALUES (:expires_at)'
);
 
$statement->execute([
    'expires_at' => $expiresAt->format('Y-m-d H:i:s.u'),
]);
در این مسیر، PHP تاریخ را به یک رشته استاندارد تبدیل می‌کند و MySQL آن را داخل DATETIME(6) قرار می‌دهد؛ هیچ int ۳۲‌بیتی در مسیر لازم نیست.

# اگر Timestamp عددی می‌خواهیم، نوع ستون باید چه باشد؟

گاهی API یا طراحی سیستم ایجاب می‌کند زمان به‌صورت Unix Timestamp ذخیره شود. این Schema مشکل دارد:
INT SIGNED ✕

بیشترین مقدار همان 2147483647 است؛ این ستون نیز در ۲۰۳۸ متوقف می‌شود.

run_at_epoch INT SIGNED NOT NULL
BIGINT SIGNED ✓

محدوده تا حدود ۲۹۲ میلیارد سال؛ مرز ۲۰۳۸ عملاً حذف می‌شود.

run_at_epoch BIGINT SIGNED NOT NULL
اگر برنامه ممکن است روی PHP ۳۲‌بیتی اجرا شود، می‌توان مقدار تولیدشده با format('U') را به‌صورت رشته عددی به PDO داد تا مجبور به تبدیل آن به int PHP نشویم:
insert_epoch.php
 
$runAt = new DateTimeImmutable('2040-01-01 00:00:00', new DateTimeZone('UTC'));
$epochAsString = $runAt->format('U'); // "2208988800"
 
$statement = $pdo->prepare('INSERT INTO jobs (run_at_epoch) VALUES (:run_at)');
$statement->bindValue(':run_at', $epochAsString, PDO::PARAM_STR);
$statement->execute();
MySQL رشته عددی را برای ستون BIGINT تبدیل می‌کند، بدون اینکه PHP مجبور باشد مقدار را داخل int ۳۲‌بیتی قرار دهد. ورودی باید از منبع معتبر ساخته یا پیش از ارسال اعتبارسنجی شود.
استفاده از INT UNSIGNED فقط مرز را از ۲۰۳۸ به 2106-02-07 منتقل می‌کند؛ راه‌حل دائمی نیست و پشتیبانی از زمان‌های پیش از ۱۹۷۰ را نیز از بین می‌برد. اگر واقعاً Timestamp عددی لازم است، BIGINT انتخاب روشن‌تری است.

# یک زنجیره فقط به اندازه ضعیف‌ترین حلقه آن امن است

ممکن است همه آزمایش‌های PHP موفق باشند، اما مقدار زمان در یکی از مراحل بعدی کوتاه شود:
مسیر داده زمان
PHP 64-bit
PDO Driver
Database Column
Queue / Cache
JSON API
Client Application
برای نمونه، PHP عدد 2208988800 را درست تولید می‌کند؛ ولی Worker قدیمی صف آن را داخل int32 می‌خواند، یا دیتابیس مقدار را در INT SIGNED نگه می‌دارد، یا یک دستگاه مقصد در Firmware خود از time_t ۳۲‌بیتی استفاده می‌کند:
PHP
PHP Runtime
Build ۳۲‌بیتی و محدودیت PHP_INT_MAX
OS
سیستم‌عامل و کتابخانه C
استفاده از نمایش زمانی ۳۲‌بیتی در محیط‌های قدیمی
SQL
MySQL
ستون TIMESTAMP یا INT SIGNED
Q/C
Cache و Queue
Serializer یا Consumer قدیمی با int32
API
API
تعریف فیلد Timestamp به‌عنوان عدد صحیح ۳۲‌بیتی
FW
دستگاه و Firmware
روتر، کنترلر صنعتی یا تجهیزات قدیمی با عمر طولانی
برای مقایسه، نوع timestamp در PostgreSQL هشت بایت است و مستندات آن بازه‌ای بسیار فراتر از ۲۰۳۸ را اعلام می‌کنند. این تفاوت نشان می‌دهد نام یک نوع داده کافی نیست؛ باید تعریف دقیق آن نوع در همان محصول بررسی شود.

# کدام سیستم‌ها واقعاً در معرض خطرند و چرا نباید صبر کنیم؟

بیشترین ریسک مربوط به سیستم‌هایی است که هم عمر طولانی دارند و هم ارتقای آن‌ها سخت است:
01
سرورها یا Containerهای قدیمی با PHP ۳۲‌بیتی
02
تجهیزات Embedded و Firmwareهای ۳۲‌بیتی
03
کنترلرهای صنعتی، تجهیزات شبکه و سامانه‌های خودرویی قدیمی
04
نرم‌افزارهای حسابداری، بیمه، اشتراک یا رزرو با تاریخ‌های بلندمدت
05
دیتابیس‌هایی که Timestamp را در INT SIGNED ذخیره می‌کنند
06
APIها و Message Schemaهایی که فیلد زمان را int32 تعریف کرده‌اند
برنامه‌ها فقط با «زمان فعلی» کار نمی‌کنند. همین امروز ممکن است موارد زیر از مرز عبور کنند: تاریخ پایان یک قرارداد ۱۵ یا ۲۰ ساله، اعتبار یک مجوز بلندمدت، تاریخ تولد و برنامه بازنشستگی، رزرو، وام، بیمه یا برنامه نگهداری تجهیزات، و تستی که تاریخ آینده را شبیه‌سازی می‌کند. اگر در سال ۲۰۲۶ یک اعتبار ۱۵ساله بسازیم، تاریخ پایان آن در ۲۰۴۱ قرار می‌گیرد. بنابراین Year 2038 از مدت‌ها قبل از رسیدن ساعت واقعی به آن روز می‌تواند وارد منطق برنامه شود.

# یک تست مرزی ساده برای پروژه PHP

assert_2038.php
 
function assertPhpSupportsPost2038Int(): void
{
    if (PHP_INT_SIZE < 8) {
        throw new RuntimeException('This PHP build uses 32-bit integers.');
    }
 
    $date = new DateTimeImmutable('2038-01-19 03:14:08', new DateTimeZone('UTC'));
 
    if ($date->getTimestamp() !== 2147483648) {
        throw new RuntimeException('Post-2038 timestamp test failed.');
    }
}
 
assertPhpSupportsPost2038Int();
echo "PHP runtime test passed.\n";
این فقط تست Runtime است. تست کامل باید مقدار را داخل دیتابیس ذخیره کند، دوباره بخواند، وارد Queue کند، به API بفرستد و در Consumer نهایی مقایسه کند. مشکل ۲۰۳۸ یک مسئله End-to-End است، نه فقط یک Unit Test برای تابع time(). تاریخ‌های پیشنهادی برای تست:
test_dates.txt
1901-12-13 20:45:52 UTC  # کمترین int32
1969-12-31 23:59:59 UTC  # یک ثانیه قبل از Epoch
1970-01-01 00:00:00 UTC  # خود Epoch
2038-01-19 03:14:07 UTC  # آخرین لحظه int32
2038-01-19 03:14:08 UTC  # اولین لحظه پس از مرز
2040-01-01 00:00:00 UTC  # مثال مقاله
2106-02-07 06:28:15 UTC  # سقف INT UNSIGNED
هر پروژه لازم نیست تمام این تاریخ‌ها را پشتیبانی کند؛ اما محدوده پشتیبانی باید آگاهانه تعریف و آزمایش شود.

# اشتباه‌های رایج هنگام حل مشکل

فقط PHP را ارتقا دهیم: اگر ستون MySQL همچنان TIMESTAMP یا INT SIGNED باشد، تاریخ ۲۰۴۰ ذخیره نمی‌شود.
چون سیستم‌عامل ۶۴‌بیتی است، PHP هم حتماً ۶۴‌بیتی است: معماری PHP در حال اجرا را با PHP_INT_SIZE بررسی کنید. محیط CLI، وب‌سرور و Worker صف ممکن است Binaryهای متفاوتی داشته باشند.
Timestamp را داخل float قرار دهیم: float راه‌حل مناسبی برای شناسه دقیق زمان نیست. با بزرگ‌شدن اعداد، دقت ممیز شناور محدود می‌شود و مقایسه یا مرتب‌سازی می‌تواند رفتار غافلگیرکننده‌ای پیدا کند.
از INT UNSIGNED استفاده کنیم و مسئله را تمام‌شده بدانیم: این کار فقط مهلت را تا سال ۲۱۰۶ افزایش می‌دهد و سازگاری با بخش‌های دیگر را تضمین نمی‌کند.
همه‌چیز را رشته کنیم، بدون قرارداد مشخص: رشته می‌تواند راه انتقال مناسبی باشد، اما Format، UTC بودن، دقت ثانیه یا میکروثانیه و نحوه اعتبارسنجی باید مشخص باشند. رشته مبهمی مثل 01/02/40 راه‌حل نیست؛ از ISO 8601 یا قالب دیتابیس تعریف‌شده استفاده کنید.
تغییر TIMESTAMP به DATETIME بدون بررسی منطقه زمانی: MySQL برای TIMESTAMP تبدیل منطقه زمانی انجام می‌دهد، اما DATETIME چنین رفتاری ندارد. مهاجرت کورکورانه می‌تواند ساعت همه رکوردها را جابه‌جا کند. پیش از Migration باید منطقه زمانی Connection، معنای داده موجود و قرارداد UTC بررسی شوند.

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

1
بررسی PHP_INT_SIZE در تمام Runtimeها
2
پیدا کردن ستون‌های TIMESTAMP، INT و فیلدهای Epoch
3
آزمایش End-to-End تاریخ‌های بعد از ۲۰۳۸
4
تاریخ تقویمی با DateTimeImmutable + قرارداد UTC
5
Epoch عددی → BIGINT | تاریخ تقویمی → DATETIME(6)
برای پروژه‌های جدید، یک الگوی قابل‌دفاع این است: محاسبه تاریخ با DateTimeImmutable، تبدیل صریح به UTC در مرز ذخیره‌سازی، استفاده از DATETIME(6) برای تاریخ‌های تقویمی آینده در MySQL، استفاده از BIGINT فقط وقتی Epoch عددی واقعاً لازم است، استفاده از یک PHP پشتیبانی‌شده با Build ۶۴‌بیتی، و آزمایش رفت‌وبرگشت تاریخ‌های ۲۰۳۸، ۲۰۴۰ و تاریخ انتهایی دامنه کسب‌وکار.

# سال ۲۰۳۸ مشکل آینده نیست؛ مشکل نوع داده امروز است

مشکل Year 2038 در ظاهر درباره روزی در آینده است، اما ریشه آن به یک تصمیم قدیمی و بسیار ساده برمی‌گردد: ذخیره تعداد ثانیه‌ها در یک عدد علامت‌دار ۳۲‌بیتی. تا وقتی مقدار از 2147483647 عبور نکرده باشد، همه‌چیز عادی به نظر می‌رسد. یک ثانیه بعد، سیستم دیگر فضایی برای نمایش مقدار جدید ندارد.
PHP در اجرای ۶۴‌بیتی از این مرز عبور می‌کند و PHP 7.0 پشتیبانی ۶۴‌بیتی را یکدست‌تر کرد؛ اما این پایان بررسی نیست. ممکن است دیتابیس، Queue، API یا Firmware هنوز زمان را در چهار بایت نگه دارد. بنابراین سؤال درست این نیست که «آیا PHP من جدید است؟»؛ سؤال درست این است که «آیا تمام مسیر داده می‌تواند تاریخ بعد از ۲۰۳۸ را بدون تغییر نگه دارد؟»
مهم‌ترین نکته همین است: زمان فقط یک تاریخ روی صفحه نیست؛ قراردادی عددی است که باید در تمام اجزای سیستم یک معنا و یک محدوده داشته باشد.
takeaway.txt
ساعت، یک روز را خراب نمی‌کند؛
یک عددِ کوچک‌تر از واقعیت کل سیستم را می‌شکند.

# مقالات مرتبط