
PHP'de Tarih ve Saat: DateTimeImmutable ile Doğru Zaman Yönetimi
Tarih işlemleri basit görünür, ancak üretimde en geç fark edilen hatalar bu alandan çıkar. Ayın son gününe bir ay eklendiğinde sonucun ne olacağı, yaz saati geçişinde bir günün kaç saat süreceği ve sunucunun zaman diliminin kullanıcınınkiyle aynı olup olmadığı, çoğu kodda örtük varsayım olarak kalır.
Bu bölümde tarih nesnelerinin doğru kullanımı, zaman dilimi yönetimi, fark hesaplama, biçimlendirme ve veritabanında saklama düzeni ele alınıyor.
Neden DateTimeImmutable
DateTime sınıfının değiştirici metotları nesnenin kendisini değiştirir. Aynı nesne birden fazla yerde kullanıldığında bir hesaplama diğerinin sonucunu bozar. DateTimeImmutable ise her işlemde yeni bir nesne döndürür ve bu sınıf hataları yapısal olarak engeller.
$baslangic = new DateTime('2026-01-31');
$bitis = $baslangic->modify('+1 month');
echo $baslangic->format('Y-m-d'); // 2026-03-03 - başlangıç da değişti
$baslangic = new DateTimeImmutable('2026-01-31');
$bitis = $baslangic->modify('+1 month');
echo $baslangic->format('Y-m-d'); // 2026-01-31 - değişmedi
echo $bitis->format('Y-m-d'); // 2026-03-03 - şubat 31 çekmediği için taştıÖrnekteki taşma davranışı dilin kusuru değil, tanımlı bir kuraldır: ocak ayının otuz birine bir ay eklenince şubatın otuz biri hesaplanır ve bu tarih mart başına denk gelir. Ay sonu hesabı yapılacaksa last day of ifadesi kullanılır.
$ay = new DateTimeImmutable('2026-01-31');
echo $ay->modify('last day of next month')->format('Y-m-d'); // 2026-02-28Zaman dilimi
Zaman dilimi belirtilmeden oluşturulan nesne, php.ini içindeki varsayılanı kullanır. Sunucu ile kullanıcının dilimi farklı olduğunda bu varsayım sessiz hatalar üretir. Genel kural: içeride UTC ile çalışılır, yalnızca gösterim aşamasında kullanıcının dilimine çevrilir.
$utc = new DateTimeImmutable('now', new DateTimeZone('UTC'));
$istanbul = $utc->setTimezone(new DateTimeZone('Europe/Istanbul'));
echo $istanbul->format('d.m.Y H:i');
// kullanıcıdan gelen yerel saati saklamak üzere UTC'ye çevirmek
$yerel = new DateTimeImmutable('2026-07-27 09:30', new DateTimeZone('Europe/Istanbul'));
echo $yerel->setTimezone(new DateTimeZone('UTC'))->format('Y-m-d H:i:s');Zaman dilimi adı, sabit bir saat farkından daha güvenlidir. Üç saat ileri biçimindeki bir kaydırma yaz saati uygulamasını hesaba katmaz; bölge adı ise geçmiş ve gelecekteki kuralları içerir.
Metinden tarih üretmek
Kullanıcıdan gelen tarih metni beklenen biçimde olmayabilir. createFromFormat biçimi açıkça tanımlar ve uyuşmazlıkta false döner. Esnek ayrıştırma yapan yapıcı metot ise belirsiz girdilerde yanlış tarih üretebilir.
$ham = '27.07.2026';
$tarih = DateTimeImmutable::createFromFormat('d.m.Y|', $ham);
// sondaki | işareti saat bölümünü sıfırlar, aksi halde o anki saat kullanılır
if ($tarih === false) {
throw new InvalidArgumentException('Tarih biçimi geçersiz');
}
// ayrıştırma uyarılarını da denetlemek gerekir: 31.02.2026 sessizce mart 3 olur
$durum = DateTimeImmutable::getLastErrors();
if ($durum !== false && ($durum['warning_count'] > 0 || $durum['error_count'] > 0)) {
throw new InvalidArgumentException('Geçersiz tarih');
}Fark hesaplama ve aralıklar
$dogum = new DateTimeImmutable('1990-04-12');
$bugun = new DateTimeImmutable('today');
$fark = $dogum->diff($bugun);
echo $fark->y; // tam yıl
echo $fark->days; // toplam gün
// sabit süre eklemek
$sonTarih = $bugun->add(new DateInterval('P30D')); // 30 gün
$hafta = $bugun->add(new DateInterval('P1W'));
// aralık üzerinde dolaşmak
$donem = new DatePeriod(
$bugun,
new DateInterval('P1D'),
$bugun->add(new DateInterval('P7D')),
);
foreach ($donem as $gun) {
echo $gun->format('Y-m-d'), PHP_EOL;
}İki tarih arasındaki farkı saniye cinsinden hesaplamak için zaman damgalarının çıkarılması yeterlidir. Gün sayısı gerekiyorsa çıkarma sonucunun sabit bir sayıya bölünmesi yanlıştır: yaz saati geçişinde bir gün 23 ya da 25 saat sürer. Gün farkı için nesne karşılaştırması kullanılmalıdır.
Tarih nesneleri karşılaştırma operatörleriyle doğrudan kıyaslanabilir. Bu, zaman damgasına çevirip sayı karşılaştırmasından hem kısa hem de zaman dilimi farklarını hesaba kattığı için doğrudur.
if ($sonKullanma < new DateTimeImmutable('now', new DateTimeZone('UTC'))) {
throw new DomainException('Kupon süresi dolmuş');
}
// listeyi tarihe göre sıralamak
usort($kayitlar, static fn (Kayit $a, Kayit $b): int => $a->tarih <=> $b->tarih);Görece zaman ve tekrarlayan işler
Arayüzde sık kullanılan görece zaman gösterimi, iki tarih arasındaki farktan üretilir. Hesaplamanın gösterim katmanında yapılması ve kaynak değerin ham biçimde saklanması önerilir; böylece aynı veri farklı arayüzlerde farklı biçimlerde sunulabilir.
function goreceZaman(DateTimeImmutable $an, DateTimeImmutable $simdi): string
{
$fark = $simdi->getTimestamp() - $an->getTimestamp();
return match (true) {
$fark < 60 => 'az önce',
$fark < 3600 => intdiv($fark, 60) . ' dakika önce',
$fark < 86400 => intdiv($fark, 3600) . ' saat önce',
$fark < 2592000 => intdiv($fark, 86400) . ' gün önce',
default => $an->format('d.m.Y'),
};
}Belirli aralıklarla çalışan işlerde yaz saati geçişi ayrı bir dikkat gerektirir. Yerel saate göre planlanan bir görev, saatlerin ileri alındığı gün hiç çalışmayabilir ya da geri alındığı gün iki kez çalışabilir. Zamanlanmış işlerin UTC üzerinden tanımlanması bu iki durumu da ortadan kaldırır.
Aynı işin iki kez çalışması ihtimaline karşı, işlem kayıtlarında benzersiz bir anahtar tutulması gerekir. Bir sonraki çalıştırmada aynı anahtar görülürse iş atlanır; böylece tekrar eden tetikleme veri bozulmasına yol açmaz.
Türkçe biçimlendirme
format metodu ay ve gün adlarını İngilizce üretir. Türkçe adlar için intl eklentisindeki biçimlendirici kullanılır; eklenti yoksa küçük bir eşleme dizisi yeterlidir.
$tarih = new DateTimeImmutable('2026-07-27');
$bicim = new IntlDateFormatter(
'tr_TR',
IntlDateFormatter::LONG,
IntlDateFormatter::NONE,
'Europe/Istanbul',
);
echo $bicim->format($tarih); // 27 Temmuz 2026
// intl yoksa
$aylar = [1 => 'Ocak', 'Şubat', 'Mart', 'Nisan', 'Mayıs', 'Haziran',
'Temmuz', 'Ağustos', 'Eylül', 'Ekim', 'Kasım', 'Aralık'];
echo $tarih->format('j') . ' ' . $aylar[(int) $tarih->format('n')] . ' ' . $tarih->format('Y');Zaman damgası ve süre ölçümü
Duvar saati ile süre ölçümü birbirinden ayrılmalıdır. Sistem saati kullanıcı ya da zaman sunucusu tarafından geriye alınabilir; iki ölçüm arasındaki fark bu yüzden negatif çıkabilir. Süre ölçümü için geriye gitmeyen sayaç kullanılır.
// yanlış: sistem saati değişirse sonuç bozulur
$basla = microtime(true);
islemYap();
$sure = microtime(true) - $basla;
// doğru: tekdüze artan sayaç, nanosaniye çözünürlüğünde
$basla = hrtime(true);
islemYap();
$sureMs = (hrtime(true) - $basla) / 1_000_000;Zaman damgası saniye çözünürlüğündedir ve zaman dilimi bilgisi taşımaz. Sıralama ve fark hesabı için elverişlidir, ancak insan tarafından okunacak her yerde nesneye çevrilmelidir. Kayıt dosyalarına yazarken ise ISO 8601 biçimi hem okunabilir hem de sıralanabilir olduğu için tercih edilir.
echo (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format(DATE_ATOM);
// 2026-07-27T06:45:12+00:00Veritabanında saklama
Zaman değerleri UTC olarak ve sıralanabilir bir biçimde saklanır. MySQL tarafında DATETIME sütunu, SQLite tarafında ISO 8601 metni uygundur. Zaman damgasının tam sayı olarak saklanması sorgu okunabilirliğini düşürür ve tarih fonksiyonlarının kullanımını zorlaştırır.
$olusturma = new DateTimeImmutable('now', new DateTimeZone('UTC'));
$sorgu = $pdo->prepare(
'INSERT INTO kayitlar (baslik, olusturma) VALUES (:baslik, :olusturma)'
);
$sorgu->execute([
'baslik' => $baslik,
'olusturma' => $olusturma->format('Y-m-d H:i:s'),
]);
// okurken tekrar nesneye çevirmek
$satir = $sorgu->fetch();
$okunan = new DateTimeImmutable($satir['olusturma'], new DateTimeZone('UTC'));Sütun tipi seçilirken zaman dilimi davranışı da göz önünde bulundurulmalıdır. MySQL tarafındaki TIMESTAMP tipi değeri sunucunun dilimine göre çevirir ve 2038 sınırı taşır; DATETIME ise çevirme yapmaz. Uygulama tarafında UTC üretiliyorsa çevirmeyen tip doğru seçimdir.
Sınıf içinde zamanı doğrudan üretmek yerine dışarıdan almak, test yazmayı kolaylaştırır. O anki zamanı sabitleyebilen bir saat arayüzü, zamana bağlı kuralların denenmesini mümkün kılar.
interface Saat
{
public function simdi(): DateTimeImmutable;
}
final class SistemSaati implements Saat
{
public function simdi(): DateTimeImmutable
{
return new DateTimeImmutable('now', new DateTimeZone('UTC'));
}
}
final class SabitSaat implements Saat // testlerde kullanılır
{
public function __construct(private readonly DateTimeImmutable $an) {}
public function simdi(): DateTimeImmutable
{
return $this->an;
}
}Kontrol listesi
- Tarih işlemlerinde
DateTimeImmutablekullanılıyor. - Zaman değerleri UTC saklanıyor, yalnızca gösterimde çevriliyor.
- Zaman dilimi sabit saat farkı yerine bölge adıyla veriliyor.
- Kullanıcı girdisi
createFromFormatile ayrıştırılıp uyarılar denetleniyor. - Gün farkı hesabı saniye bölmesiyle değil nesne karşılaştırmasıyla yapılıyor.
- O anki zaman bir arayüz üzerinden alınıyor, böylece testte sabitlenebiliyor.
İlgili yazılar
PHP eğitim serisi
Bu yazı, temel sözdiziminden üretim ortamına kadar ilerleyen 25 bölümlük serinin bir parçası. Bölümler sırayla okunacak biçimde hazırlandı, ancak her biri tek başına da kullanılabilir.
Temeller
- Kurulum ve temel sözdizimi
- Döngüler ve akış kontrolü
- Diziler
- Fonksiyonlar
- String fonksiyonları
- Süper globaller
- Form işleme
- Dosya işlemleri
Dilin ayrıntıları
- Tip sistemi ve strict_types
- Hata ve istisna yönetimi
- Düzenli ifadeler
- Tarih ve saat işlemleri (bu yazı)
Nesne yönelimli PHP
- Sınıf ve nesne
- Kalıtım, arayüz ve trait
- Statik üyeler, sabitler ve enum
- Namespace, Composer ve autoload
Veri ve güvenlik
- PDO ile veritabanı işlemleri
- Oturum yönetimi ve kimlik doğrulama
- Güvenli çıktı ve XSS
- JSON ile veri değişimi
Dış dünya ve üretim
Yorumlar
İlk yorumu sen yap.
İlgili Yazılar

PHP Uygulamasını Üretime Almak: Yapılandırma ve Güvenlik
Geliştirme makinesinde çalışan kod ile üretimde güvenle çalışan kod arasındaki fark yapılandırmadır. Ayarlar, izinler, dağıtım akışı ve son kontroller.

PHP ile REST API Yazmak: Yönlendirme ve HTTP Durum Kodları
Bir API'nin kalitesi çoğunlukla ayrıntılarda belli olur: doğru durum kodu, tutarlı hata biçimi ve öngörülebilir adresler. Saf PHP ile uçtan uca kurulum.

PHPUnit ile Birim Test Yazmak: Kurulum, Sahte Nesneler ve Kapsam
Testin amacı hata bulmak değil, değişikliği güvenle yapabilmektir. Kurulum, ilk test, sahte nesneler ve test edilebilir kodun özellikleri.