Date & Time #
Bekerja dengan tanggal dan waktu adalah salah satu area di PHP yang paling banyak jebakannya — timezone yang tidak konsisten, format yang berbeda-beda antar sistem, DST (Daylight Saving Time) yang menggeser jam secara tidak terduga, dan perbandingan yang salah karena tidak mempertimbangkan zona waktu. PHP menyediakan dua kelas utama: DateTime yang mutable (bisa dimodifikasi di tempat) dan DateTimeImmutable yang lebih aman karena setiap operasi menghasilkan objek baru. Artikel ini membahas keduanya secara mendalam — termasuk DateInterval, DatePeriod untuk iterasi rentang tanggal, semua format karakter penting, strategi timezone yang benar, dan jebakan-jebakan yang sering menyebabkan bug di aplikasi production.
DateTime vs DateTimeImmutable #
Perbedaan paling penting yang perlu dipahami sebelum mulai: DateTime adalah mutable — method add(), sub(), modify() mengubah objek yang sama. DateTimeImmutable bersifat immutable — setiap method mengembalikan objek baru tanpa mengubah yang lama.
<?php
// DateTime — MUTABLE, memodifikasi objek asli
$dt = new \DateTime('2024-01-15');
$besok = $dt->add(new \DateInterval('P1D')); // memodifikasi $dt!
echo $dt->format('Y-m-d'); // 2024-01-16 — $dt BERUBAH!
echo $besok->format('Y-m-d'); // 2024-01-16 — $besok dan $dt adalah OBJEK YANG SAMA
// DateTimeImmutable — IMMUTABLE, selalu kembalikan objek baru
$dti = new \DateTimeImmutable('2024-01-15');
$besok = $dti->add(new \DateInterval('P1D')); // $dti TIDAK berubah
echo $dti->format('Y-m-d'); // 2024-01-15 — tetap sama
echo $besok->format('Y-m-d'); // 2024-01-16 — objek baru
Selalu gunakanDateTimeImmutablesebagai pilihan default — perilakunya lebih mudah diprediksi dan mencegah bug halus akibat mutasi objek yang tidak disengaja. GunakanDateTimehanya jika ada alasan spesifik yang membutuhkan mutasi in-place.
Membuat Objek DateTime #
Ada beberapa cara membuat objek datetime tergantung sumber datanya:
<?php
// Waktu saat ini
$sekarang = new \DateTimeImmutable();
$sekarang = new \DateTimeImmutable('now');
// Dari string relatif
$besok = new \DateTimeImmutable('+1 day');
$mingguLalu = new \DateTimeImmutable('-1 week');
$awalBulan = new \DateTimeImmutable('first day of this month');
$akhirBulan = new \DateTimeImmutable('last day of this month');
$seninDepan = new \DateTimeImmutable('next Monday');
// Dari string tanggal standar
$tanggal = new \DateTimeImmutable('2024-03-15');
$datetime = new \DateTimeImmutable('2024-03-15 14:30:00');
$iso8601 = new \DateTimeImmutable('2024-03-15T14:30:00+07:00');
// Dari format kustom — paling aman untuk input user
$dari_slash = \DateTimeImmutable::createFromFormat('d/m/Y', '15/03/2024');
$dari_indo = \DateTimeImmutable::createFromFormat('d F Y', '15 Maret 2024');
// Catatan: nama bulan Indonesia tidak didukung langsung — perlu konversi manual
// Dari Unix timestamp
$dariTimestamp = new \DateTimeImmutable('@1710504000'); // @ prefix untuk timestamp
$juga = (new \DateTimeImmutable())->setTimestamp(1710504000);
// Dapatkan timestamp dari objek
echo $sekarang->getTimestamp(); // Unix timestamp (detik sejak 1970-01-01 UTC)
createFromFormat — Parsing yang Aman
#
createFromFormat adalah cara yang jauh lebih aman untuk mem-parse string tanggal dari input user dibanding membiarkan PHP menebak formatnya sendiri:
<?php
// ANTI-PATTERN: biarkan PHP menebak format — bisa salah interpretasi
$salah = new \DateTimeImmutable('03/04/2024');
// Apakah ini 3 April atau 4 Maret? Tergantung locale!
// BENAR: format eksplisit
$benar = \DateTimeImmutable::createFromFormat('d/m/Y', '03/04/2024');
// 3 April 2024 — jelas
// Tangkap error parsing
$hasil = \DateTimeImmutable::createFromFormat('Y-m-d', 'bukan-tanggal');
if ($hasil === false) {
$errors = \DateTimeImmutable::getLastErrors();
echo "Parsing gagal: " . implode(', ', $errors['errors']);
}
// Format dengan waktu
$daftarFormat = [
'Y-m-d', // 2024-03-15
'd/m/Y', // 15/03/2024
'd-m-Y H:i:s', // 15-03-2024 14:30:00
'D, d M Y H:i:s O', // Fri, 15 Mar 2024 14:30:00 +0700
'U', // Unix timestamp
];
Memformat Tanggal #
Method format() mengubah objek datetime menjadi string. Karakter format PHP mengikuti konvensi dari fungsi C strftime, dengan beberapa tambahan:
Karakter Format Penting #
| Karakter | Keterangan | Contoh Output |
|---|---|---|
Y | Tahun 4 digit | 2024 |
y | Tahun 2 digit | 24 |
m | Bulan 2 digit | 03 |
n | Bulan tanpa leading zero | 3 |
M | Nama bulan singkat (Inggris) | Mar |
F | Nama bulan penuh (Inggris) | March |
d | Hari 2 digit | 05 |
j | Hari tanpa leading zero | 5 |
D | Nama hari singkat (Inggris) | Fri |
l | Nama hari penuh (Inggris) | Friday |
N | Hari dalam minggu (1=Senin, 7=Minggu) | 5 |
w | Hari dalam minggu (0=Minggu, 6=Sabtu) | 5 |
H | Jam 24-hour 2 digit | 14 |
G | Jam 24-hour tanpa leading zero | 14 |
h | Jam 12-hour 2 digit | 02 |
i | Menit 2 digit | 30 |
s | Detik 2 digit | 00 |
A | AM atau PM | PM |
a | am atau pm | pm |
U | Unix timestamp | 1710504000 |
W | Nomor minggu dalam tahun (ISO 8601) | 11 |
t | Jumlah hari dalam bulan | 31 |
L | Apakah tahun kabisat (1/0) | 1 |
Z | Offset timezone dalam detik | 25200 |
P | Offset timezone +HH:MM | +07:00 |
O | Offset timezone +HHMM | +0700 |
e | Nama timezone | Asia/Jakarta |
c | ISO 8601 lengkap | 2024-03-15T14:30:00+07:00 |
r | RFC 2822 | Fri, 15 Mar 2024 14:30:00 +0700 |
<?php
$dt = new \DateTimeImmutable('2024-03-15 14:30:45', new \DateTimeZone('Asia/Jakarta'));
echo $dt->format('Y-m-d H:i:s'); // 2024-03-15 14:30:45
echo $dt->format('d/m/Y'); // 15/03/2024
echo $dt->format('l, d F Y'); // Friday, 15 March 2024
echo $dt->format('H:i'); // 14:30
echo $dt->format('g:i A'); // 2:30 PM
echo $dt->format('c'); // 2024-03-15T14:30:45+07:00
echo $dt->format('U'); // Unix timestamp
// Format untuk database MySQL
echo $dt->format('Y-m-d H:i:s'); // 2024-03-15 14:30:45 — datetime
echo $dt->format('Y-m-d'); // 2024-03-15 — date only
// Format untuk tampilan Indonesia
function formatIndonesia(\DateTimeInterface $dt): string
{
$bulan = [
1 => 'Januari', 2 => 'Februari', 3 => 'Maret',
4 => 'April', 5 => 'Mei', 6 => 'Juni',
7 => 'Juli', 8 => 'Agustus', 9 => 'September',
10 => 'Oktober',11 => 'November', 12 => 'Desember',
];
$hari = [
1 => 'Senin', 2 => 'Selasa', 3 => 'Rabu',
4 => 'Kamis', 5 => 'Jumat', 6 => 'Sabtu', 7 => 'Minggu',
];
return sprintf(
'%s, %d %s %d',
$hari[(int) $dt->format('N')],
(int) $dt->format('j'),
$bulan[(int) $dt->format('n')],
(int) $dt->format('Y')
);
}
echo formatIndonesia($dt); // Jumat, 15 Maret 2024
DateInterval — Mewakili Selang Waktu #
DateInterval merepresentasikan durasi waktu — bukan titik waktu. Notasinya mengikuti standar ISO 8601 dengan prefix P (Period):
P[tahun]Y[bulan]M[hari]DT[jam]H[menit]M[detik]S
Bagian T memisahkan komponen tanggal dari waktu.
<?php
// Contoh notasi ISO 8601
$1hari = new \DateInterval('P1D'); // 1 hari
$1minggu = new \DateInterval('P1W'); // 1 minggu (= 7 hari)
$1bulan = new \DateInterval('P1M'); // 1 bulan
$1tahun = new \DateInterval('P1Y'); // 1 tahun
$2tahun3bulan = new \DateInterval('P2Y3M'); // 2 tahun 3 bulan
$90menit = new \DateInterval('PT90M'); // 90 menit (T sebelum komponen waktu)
$1jam30menit = new \DateInterval('PT1H30M'); // 1 jam 30 menit
$komplit = new \DateInterval('P1Y2M3DT4H5M6S'); // lengkap
// Tambah dan kurang dengan DateInterval
$sekarang = new \DateTimeImmutable('2024-01-15 10:00:00');
$besok = $sekarang->add(new \DateInterval('P1D'));
$kemarin = $sekarang->sub(new \DateInterval('P1D'));
$bulanDepan = $sekarang->add(new \DateInterval('P1M'));
$2jamLagi = $sekarang->add(new \DateInterval('PT2H'));
echo $besok->format('Y-m-d'); // 2024-01-16
echo $kemarin->format('Y-m-d'); // 2024-01-14
echo $bulanDepan->format('Y-m-d'); // 2024-02-15
echo $2jamLagi->format('H:i'); // 12:00
modify() — Modifikasi dengan String Relatif
#
Alternatif dari DateInterval untuk operasi sederhana:
<?php
$dt = new \DateTimeImmutable('2024-03-15 14:30:00');
echo $dt->modify('+1 day')->format('Y-m-d'); // 2024-03-16
echo $dt->modify('+1 month')->format('Y-m-d'); // 2024-04-15
echo $dt->modify('+1 year')->format('Y-m-d'); // 2025-03-15
echo $dt->modify('next Monday')->format('Y-m-d'); // senin berikutnya
echo $dt->modify('first day of next month')->format('Y-m-d'); // 2024-04-01
echo $dt->modify('last day of this month')->format('Y-m-d'); // 2024-03-31
echo $dt->modify('midnight')->format('Y-m-d H:i:s'); // 2024-03-15 00:00:00
echo $dt->modify('noon')->format('Y-m-d H:i:s'); // 2024-03-15 12:00:00
// ANTI-PATTERN: +1 month pada akhir bulan — hasilkan tanggal yang mengejutkan
$akhirJanuari = new \DateTimeImmutable('2024-01-31');
echo $akhirJanuari->modify('+1 month')->format('Y-m-d'); // 2024-03-02!
// Januari punya 31 hari, Februari punya 28/29 — PHP "overflow" ke Maret
// BENAR: gunakan 'first day of next month' jika butuh awal bulan berikutnya
echo $akhirJanuari->modify('first day of next month')->format('Y-m-d'); // 2024-02-01
Jebakan+1 month— PHP menambahkan 1 ke nilai bulan secara literal, lalu menormalisasi tanggalnya. Jika hari melebihi jumlah hari di bulan tujuan, PHP “overflow” ke bulan berikutnya.2024-01-31 + 1 month = 2024-03-02karena Februari 2024 hanya 29 hari. Untuk navigasi bulanan yang andal, selalu reset ke hari pertama atau gunakanfirst day of next month.
Menghitung Selisih — diff()
#
Method diff() mengembalikan DateInterval yang mewakili perbedaan antara dua datetime:
<?php
$lahir = new \DateTimeImmutable('1995-08-17');
$sekarang = new \DateTimeImmutable('2024-03-15');
$selisih = $lahir->diff($sekarang);
echo $selisih->y; // 28 — tahun
echo $selisih->m; // 6 — bulan sisanya
echo $selisih->d; // 26 — hari sisanya
echo $selisih->days; // 10437 — total hari (property khusus)
echo $selisih->h; // jam sisanya
echo $selisih->invert; // 0 jika positif, 1 jika negatif (masa lalu ke masa depan)
// Format diff
echo $selisih->format('%y tahun %m bulan %d hari');
// "28 tahun 6 bulan 26 hari"
echo $selisih->format('%R%a hari');
// "+10437 hari" — %R untuk tanda, %a untuk total hari
// Hitung umur
function hitungUmur(\DateTimeInterface $tanggalLahir): int
{
return $tanggalLahir->diff(new \DateTimeImmutable())->y;
}
echo hitungUmur(new \DateTimeImmutable('1995-08-17')); // umur dalam tahun
// Cek apakah sudah lewat deadline
function sudahLewat(\DateTimeInterface $deadline): bool
{
return $deadline < new \DateTimeImmutable();
}
// Selisih dalam satuan yang spesifik
function selisihDalamJam(\DateTimeInterface $a, \DateTimeInterface $b): float
{
return abs($a->getTimestamp() - $b->getTimestamp()) / 3600;
}
function selisihDalamHari(\DateTimeInterface $a, \DateTimeInterface $b): int
{
return (int) abs($a->diff($b)->days);
}
$mulai = new \DateTimeImmutable('2024-03-01');
$selesai = new \DateTimeImmutable('2024-03-15');
echo selisihDalamHari($mulai, $selesai); // 14
DatePeriod — Iterasi Rentang Tanggal #
DatePeriod memungkinkan iterasi atas rentang tanggal dengan interval tertentu — sangat berguna untuk membuat kalender, laporan mingguan, atau jadwal berulang:
<?php
// Iterasi setiap hari dalam bulan Maret 2024
$mulai = new \DateTimeImmutable('2024-03-01');
$akhir = new \DateTimeImmutable('2024-04-01'); // tidak inklusif
$interval = new \DateInterval('P1D');
$periode = new \DatePeriod($mulai, $interval, $akhir);
foreach ($periode as $tanggal) {
echo $tanggal->format('Y-m-d l') . "\n";
// 2024-03-01 Friday
// 2024-03-02 Saturday
// ... sampai 2024-03-31
}
// Iterasi hari kerja saja (Senin-Jumat)
foreach ($periode as $tanggal) {
$hariKe = (int) $tanggal->format('N'); // 1=Senin, 7=Minggu
if ($hariKe <= 5) {
echo $tanggal->format('d/m/Y') . "\n";
}
}
// Iterasi setiap minggu
$periodeMingguan = new \DatePeriod(
new \DateTimeImmutable('2024-01-01'),
new \DateInterval('P1W'),
new \DateTimeImmutable('2024-12-31')
);
$weekCount = iterator_count($periodeMingguan);
echo "Jumlah minggu di 2024: $weekCount"; // 52
// Iterasi setiap bulan
$periodeBulanan = new \DatePeriod(
new \DateTimeImmutable('2024-01-01'),
new \DateInterval('P1M'),
new \DateTimeImmutable('2025-01-01')
);
foreach ($periodeBulanan as $bulan) {
$awal = $bulan->format('Y-m-01');
$akhirBulan = $bulan->modify('last day of this month')->format('Y-m-d');
echo "$awal sampai $akhirBulan\n";
}
// Buat slot jadwal setiap 30 menit dalam sehari
$slotJadwal = new \DatePeriod(
new \DateTimeImmutable('2024-03-15 08:00:00'),
new \DateInterval('PT30M'),
new \DateTimeImmutable('2024-03-15 17:00:00')
);
foreach ($slotJadwal as $slot) {
echo $slot->format('H:i') . "\n"; // 08:00, 08:30, 09:00, ...
}
Timezone — Menghindari Masalah Zona Waktu #
Timezone adalah sumber bug yang paling umum dalam aplikasi yang melayani pengguna dari berbagai wilayah. Strategi yang benar: simpan semua waktu dalam UTC di database, konversi ke timezone lokal hanya saat ditampilkan.
<?php
// Atur timezone default aplikasi — lakukan sekali di bootstrap
date_default_timezone_set('UTC'); // Selalu UTC untuk penyimpanan
// Atau via php.ini:
// date.timezone = UTC
// Buat datetime dengan timezone eksplisit
$utc = new \DateTimeZone('UTC');
$jakarta = new \DateTimeZone('Asia/Jakarta'); // WIB — UTC+7
$bali = new \DateTimeZone('Asia/Makassar'); // WITA — UTC+8
$papua = new \DateTimeZone('Asia/Jayapura'); // WIT — UTC+9
// Waktu saat ini dalam berbagai zona
$sekarangUtc = new \DateTimeImmutable('now', $utc);
$sekarangJakarta = new \DateTimeImmutable('now', $jakarta);
echo $sekarangUtc->format('H:i T'); // 07:00 UTC
echo $sekarangJakarta->format('H:i T'); // 14:00 WIB
// Konversi antar timezone
$eventUtc = new \DateTimeImmutable('2024-03-15 07:00:00', $utc);
$eventJakarta = $eventUtc->setTimezone($jakarta);
$eventBali = $eventUtc->setTimezone($bali);
echo $eventJakarta->format('Y-m-d H:i T'); // 2024-03-15 14:00 WIB
echo $eventBali->format('Y-m-d H:i T'); // 2024-03-15 15:00 WITA
// Workflow yang benar: terima input → konversi ke UTC → simpan
function simpanEvent(string $tanggalLokal, string $timezone): string
{
$tz = new \DateTimeZone($timezone);
$local = new \DateTimeImmutable($tanggalLokal, $tz);
$utc = $local->setTimezone(new \DateTimeZone('UTC'));
return $utc->format('Y-m-d H:i:s'); // simpan ini ke database
}
function tampilkanEvent(string $tanggalUtc, string $timezone): string
{
$utc = new \DateTimeImmutable($tanggalUtc, new \DateTimeZone('UTC'));
$local = $utc->setTimezone(new \DateTimeZone($timezone));
return $local->format('d/m/Y H:i T'); // tampilkan ini ke user
}
$disimpan = simpanEvent('2024-03-15 14:00:00', 'Asia/Jakarta');
echo $disimpan; // 2024-03-15 07:00:00 (UTC)
echo tampilkanEvent($disimpan, 'Asia/Jakarta'); // 15/03/2024 14:00 WIB
echo tampilkanEvent($disimpan, 'Asia/Makassar'); // 15/03/2024 15:00 WITA
echo tampilkanEvent($disimpan, 'America/New_York'); // 15/03/2024 03:00 EDT
Daftar Timezone Indonesia #
<?php
$timezoneIndonesia = [
'Asia/Jakarta' => 'WIB (UTC+7) — Sumatera, Jawa, Kalimantan Barat & Tengah',
'Asia/Makassar' => 'WITA (UTC+8) — Bali, NTB, NTT, Kalimantan Selatan, Timur & Utara, Sulawesi',
'Asia/Jayapura' => 'WIT (UTC+9) — Papua & Maluku',
];
foreach ($timezoneIndonesia as $tz => $keterangan) {
$dt = new \DateTimeImmutable('now', new \DateTimeZone($tz));
echo $dt->format('H:i') . " $tz — $keterangan\n";
}
Fungsi date() dan time() Warisan
#
PHP juga memiliki fungsi prosedural yang lebih lama. Masih banyak digunakan di kode yang lebih lama:
<?php
// time() — Unix timestamp saat ini (detik sejak 1970-01-01 00:00:00 UTC)
$timestamp = time(); // e.g., 1710504000
// date() — format timestamp menjadi string
echo date('Y-m-d H:i:s'); // waktu lokal saat ini
echo date('Y-m-d H:i:s', $timestamp); // dari timestamp tertentu
// mktime() — buat timestamp dari komponen waktu
$ts = mktime(14, 30, 0, 3, 15, 2024); // jam, menit, detik, bulan, hari, tahun
echo date('Y-m-d H:i:s', $ts); // 2024-03-15 14:30:00
// strtotime() — parse string waktu ke timestamp
$ts2 = strtotime('2024-03-15');
$ts3 = strtotime('+1 day');
$ts4 = strtotime('next Monday');
$ts5 = strtotime('2024-01-15 +1 month');
// Konversi antara timestamp dan DateTime
$dt = new \DateTimeImmutable('@' . time()); // timestamp → DateTime
$ts = (new \DateTimeImmutable())->getTimestamp(); // DateTime → timestamp
// checkdate() — validasi tanggal
var_dump(checkdate(2, 29, 2024)); // true — 2024 adalah tahun kabisat
var_dump(checkdate(2, 29, 2023)); // false — 2023 bukan kabisat
var_dump(checkdate(13, 1, 2024)); // false — bulan 13 tidak ada
Jebakan Umum Saat Bekerja dengan Waktu #
<?php
// ✗ Jebakan 1: membandingkan datetime dengan string langsung
$dt = new \DateTimeImmutable('2024-03-15');
$string = '2024-03-15';
// Ini TIDAK bekerja seperti yang diharapkan:
var_dump($dt == $string); // false — tidak bisa dibandingkan langsung
// BENAR: bandingkan objek dengan objek, atau string dengan string
var_dump($dt->format('Y-m-d') === $string); // true
var_dump($dt === new \DateTimeImmutable('2024-03-15')); // false — instance beda
var_dump($dt == new \DateTimeImmutable('2024-03-15')); // true — nilai sama
// ✗ Jebakan 2: lupa timezone saat parsing
// Ini menggunakan timezone default PHP, mungkin bukan yang dimaksud
$dt1 = new \DateTimeImmutable('2024-03-15 14:00:00');
// BENAR: selalu eksplisit soal timezone
$dt2 = new \DateTimeImmutable('2024-03-15 14:00:00', new \DateTimeZone('Asia/Jakarta'));
// ✗ Jebakan 3: menyimpan timestamp lokal di database
// Saat server pindah timezone atau ada DST, semua data jadi salah
$waktuLokal = date('Y-m-d H:i:s'); // ANTI-PATTERN: waktu lokal ke DB
// BENAR: selalu simpan UTC
$waktuUtc = gmdate('Y-m-d H:i:s'); // gmdate() selalu UTC
// Atau:
$waktuUtc = (new \DateTimeImmutable('now', new \DateTimeZone('UTC')))->format('Y-m-d H:i:s');
// ✗ Jebakan 4: +1 month pada tanggal akhir bulan
$akhirJan = new \DateTimeImmutable('2024-01-31');
echo $akhirJan->modify('+1 month')->format('Y-m-d'); // 2024-03-02 — bukan Februari!
// BENAR: gunakan first day of next month jika ingin awal bulan
echo $akhirJan->modify('first day of next month')->format('Y-m-d'); // 2024-02-01
// ✗ Jebakan 5: asumsi semua tahun punya 365 hari
// Tahun kabisat punya 366 hari, ini mempengaruhi kalkulasi
$awal2024 = new \DateTimeImmutable('2024-01-01');
$awal2025 = new \DateTimeImmutable('2025-01-01');
echo $awal2024->diff($awal2025)->days; // 366 — 2024 adalah tahun kabisat!
Ringkasan #
DateTimeImmutablelebih aman dariDateTime— setiap operasi menghasilkan objek baru, tidak ada mutasi tersembunyi. Jadikan default pilihan untuk semua kode baru.createFromFormat()untuk input user — lebih aman dari konstruktor karena format eksplisit mencegah ambiguitas interpretasi tanggal. Selalu tangkapfalsesebagai return value jika parsing gagal.- UTC untuk penyimpanan, lokal untuk tampilan — simpan semua waktu dalam UTC di database, konversi ke timezone pengguna hanya saat ditampilkan ke layar.
DateIntervaldengan notasi ISO 8601 —P1Y2M3DT4H5M6Suntuk durasi kompleks.P1D= 1 hari,PT1H= 1 jam,P1W= 1 minggu.- Jebakan
+1 monthpada akhir bulan menyebabkan overflow ke bulan berikutnya. Gunakan'first day of next month'untuk navigasi bulanan yang andal.DatePerioduntuk iterasi rentang tanggal — jauh lebih bersih dari loop manual dengan increment hari. Berguna untuk kalender, laporan periodik, dan slot jadwal.diff()mengembalikanDateInterval— gunakan property->y,->m,->duntuk komponen terpisah, atau->daysuntuk total hari. Property->invertbernilai 1 jika tanggal pertama lebih besar.gmdate()atausetTimezone(UTC)untuk mendapatkan waktu UTC dari fungsi prosedural atau sebelum menyimpan ke database.