PSR-20: زمان رو تو PHP تستپذیر کن
خلاصهٔ کاملتر
مقاله با یه سناریوی آشنا شروع میشه: یه تست بیربط تو CI با خطای «یه ثانیه اختلاف» fail میشه، دوباره اجرا میکنی و باز fail، بار سوم سبز. به گفتهٔ نویسنده مقصر معمولاً زمانه. همون چند میلیثانیهای که ساختن fixture و اجرای assertionها طول میکشه، میتونه «الان + ۳۰ ثانیه» رو به «الان + ۳۱» تبدیل کنه.
حرف اصلی نویسنده اینه که زمان یه وابستگی بیرونیه. date() و time() و DateTimeImmutable تو خود PHP هستن، ولی زمان رو از سیستمعامل میگیرن. یعنی «الان» هم به اندازهٔ یه کوئری دیتابیس یا یه درخواست HTTP بیرونیه. پس همونطور که دیتابیس رو پشت repository میذاریم، زمان رو هم باید پشت یه abstraction گذاشت.
اینجا PSR-20 میاد وسط، یه استاندارد از PHP-FIG که فقط یه اینترفیس با یه متد داره و با composer require psr/clock نصب میشه:
interface ClockInterface
{
public function now(): \DateTimeImmutable;
}مثال مقاله یه TokenFactory هست که توکنی با انقضای ۳۰ دقیقه میسازه. اگه داخلش new DateTimeImmutable('now') صدا بزنی، تستش گاهی بهخاطر جابهجایی زمان حین اجرا fail میشه. ولی وقتی ClockInterface تو constructor تزریق بشه و زمان از $this->clock->now() بیاد، کنترل زمان دست خودته.
نویسنده سه نوع پیادهسازی معرفی میکنه. SystemClock زمان واقعی رو برمیگردونه و مال پروداکشنه. MockClock (یا FrozenClock) یه زمان ثابت برمیگردونه و با متدی مثل travelTo() میشه جلو بردش، مثلاً ۳۱ دقیقه بعد رو شبیهسازی کنی و ببینی توکن منقضی شده. MonotonicClock هم برای اندازهگیری مدت زمانه و بر پایهٔ hrtime() (اضافهشده تو PHP 7.3) کار میکنه، یعنی یه شمارندهٔ داخلی که فقط جلو میره و تغییر ساعت سیستم روش اثر نداره. Symfony از نسخهٔ 6.2 یه نمونهاش رو داره.
نویسنده توضیح میده چرا microtime() برای اندازهگیری مدت مناسب نیست: چون به ساعت سیستم وابستهست، اگه ساعت عوض بشه حتی ممکنه نتیجه منفی بشه. مقدار hrtime() هم timestamp نیست و فقط برای حساب کردن مدت به درد میخوره. دربارهٔ timezone هم PSR-20 عمداً چیزی نمیگه. پیشنهاد نویسنده اینه که همهچی داخل اپ UTC باشه و فقط موقع نمایش، لاگ یا ایمیل به وقت محلی تبدیل بشه. البته برای زمانهای آیندهٔ کاربر (مثل رویدادها) بهتره زمان محلی و timezone اصلی رو ذخیره کنی.
در جواب ایرادهای رایج هم میگه برای یه اسکریپت یهبارمصرف لازم نیست، از جاهایی شروع کن که درد داره (همون تستهای شکننده)، و با autowiring تو Laravel و Symfony تزریقش تقریباً زحمتی نداره. Carbon هم امکان تغییر زمان تو تست رو میده، ولی یه وابستگی دیگه به کدت اضافه میکنه. اگه دوستش داری، میتونی خروجی Clock رو تو CarbonImmutable::instance() بپیچی.
نکات کلیدی:
- PSR-20 فقط یه متد now(): DateTimeImmutable داره و پکیجش psr/clock هست.
- SystemClock برای پروداکشن و MockClock با متد travelTo() برای تست استفاده میشه.
- برای اندازهگیری مدت، hrtime() (از PHP 7.3) جای microtime() بهتره چون تغییر ساعت سیستم روش اثر نداره.
- Symfony از نسخهٔ 6.2 یه MonotonicClock آماده داره.
- PSR-20 دربارهٔ timezone چیزی تعیین نمیکنه و نویسنده UTC رو داخل اپ پیشنهاد میده.




