İçeriğe geç
Can Uğurlu
Geri dön

Tarihi UTC sakla, gösterimde çevir

Her zaman damgasını UTC saklayın, yerel saate sadece gösterirken çevirin. Bunun tersini yapmak, günün sadece belirli saatlerinde ortaya çıkan ve bu yüzden bulması zor olan hatalar üretiyor.

Karışıklık neden oradan çıkıyor

Bir sistemde üç ayrı saat dilimi olabiliyor: sunucunun işletim sistemi saat dilimi, veritabanı bağlantısının saat dilimi, uygulamanın config dosyasındaki saat dilimi. Üçü aynı değerde değilse, bir tarih üç farklı yerde üç farklı şekilde yorumlanabiliyor.

Bug genelde gece yarısına yakın saatlerde çıkıyor. “Bugünün siparişleri” sorgusu bir günü eksik ya da fazla kapsıyor, ama bu sadece +03:00 ile UTC arasındaki üç saatlik farkın içine denk gelen zaman diliminde görünüyor. Gündüz test edildiğinde hata görünmüyor, çünkü o saatlerde iki saat dilimi aynı günü gösteriyor.

Kural: UTC sakla, gösterimde çevir

Veritabanına yazılan her tarih UTC olmalı. Kullanıcıya gösterilecek tarih, sadece o an, sadece o katmanda yerel saate çevrilir.

// Yanlış: sunucunun yerel saatiyle kaydediyor
$order->shipped_at = now();

// Doğru: UTC'de sakla, config('app.timezone') = 'UTC' olduğu sürece now() zaten UTC döner
$order->shipped_at = now();

İki satır aynı görünüyor, ama fark config/app.php içindeki timezone değerinde:

// config/app.php
'timezone' => 'UTC',

Bu değer UTC olduğu sürece Laravel’in now() yardımcı fonksiyonu ve Eloquent’in datetime cast’i veritabanına UTC yazıyor. Değer Europe/Istanbul olarak bırakılırsa uygulama +03:00 ile çalışmaya başlıyor ve bu, saklanan her tarihe sessizce karışıyor.

Gösterim katmanında çevirme işlemi tek satır:

$order->shipped_at->timezone('Europe/Istanbul')->format('d.m.Y H:i');

Bu satır sadece Blade’de ya da API response’unu hazırlarken çalışır. Veritabanı hiçbir zaman +03:00 görmez.

DST tuzağı: Türkiye artık +03, ama karşı taraf değil

Türkiye 2016’dan beri yaz saati uygulamasını kaldırdı, yıl boyu sabit +03:00. Bu bir kısmi rahatlık sağlıyor: sunucu Türkiye saatinde çalışıyorsa yıl içinde offset değişmiyor.

Ama entegre olunan pazaryerleri ve üçüncü parti API’ler aynı kuralı takip etmiyor. Avrupa merkezli bir servis yaz saati uyguluyorsa, o API’den gelen bir tarih yılın farklı zamanlarında farklı offset taşıyabilir. 2025-03-15T10:00:00+01:00 ile 2025-08-15T10:00:00+02:00 aynı sistemden gelmiş olabilir, offset’i görmeden bunu ayırt edemezsiniz.

Bu yüzden gelen her tarihte offset’i okumak, onu yok saymak değil.

MySQL TIMESTAMP ile DATETIME farklı davranıyor

CREATE TABLE orders (
    created_at TIMESTAMP,
    shipped_at DATETIME
);

TIMESTAMP sütunu, bağlantının time_zone ayarına göre UTC’ye çevrilerek saklanıyor, okunurken tekrar bağlantının saat dilimine çevriliyor. Bağlantının saat dilimi değişirse aynı satırdaki aynı değer farklı okunuyor.

DATETIME sütunu hiçbir çeviri yapmıyor. Ne yazdıysanız o saklanıyor, ne saklandıysa o okunuyor. Saat dilimi bilgisi sütunun kendisinde yok.

Bu fark, “UTC sakla” kuralını nasıl uyguladığınızı belirliyor: DATETIME kullanıyorsanız UTC’ye çevirme sorumluluğu tamamen uygulama katmanında. TIMESTAMP kullanıyorsanız bağlantının time_zone ayarı da denkleme giriyor. O ayar +00:00 değilse, uygulama UTC yazdığını sansa da veritabanı farklı bir değer saklıyor olabilir.

API sınırında ISO 8601 ile offset

{
  "shipped_at": "2025-12-20T21:00:00+03:00"
}

Offset’siz bir tarih, 2025-12-20 21:00:00, belirsiz veridir. Bu saat hangi saat dilimine göre 21:00: gönderen sistemin sunucu saati mi, UTC mi, alıcının varsayımı mı? Cevap veriden çıkmıyor, dokümantasyondan ya da tahminden çıkıyor.

Dışarıya gönderirken de, dışarıdan alırken de offset her zaman yazılmalı. Carbon::parse() offset’li bir string’i doğru yorumluyor; offset’siz bir string geldiğinde ise uygulamanın kendi config('app.timezone') değerini varsayıyor. Bu varsayım göndericinin niyetiyle örtüşmeyebilir.

Özet

config/app.php içinde timezone değerini UTC tutun. Veritabanına her zaman UTC yazın, yerel saate sadece gösterim anında çevirin. API sınırında offset’siz tarih kabul etmeyin ya da göndermeyin.


Bu yazıyı paylaş:

Önceki Yazı
Queue worker'ı Supervisor ile çalıştırın
Sonraki Yazı
Retry stratejisi: üstel geri çekilme ve jitter