Date & Time

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 gunakan DateTimeImmutable sebagai pilihan default — perilakunya lebih mudah diprediksi dan mencegah bug halus akibat mutasi objek yang tidak disengaja. Gunakan DateTime hanya 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 #

KarakterKeteranganContoh Output
YTahun 4 digit2024
yTahun 2 digit24
mBulan 2 digit03
nBulan tanpa leading zero3
MNama bulan singkat (Inggris)Mar
FNama bulan penuh (Inggris)March
dHari 2 digit05
jHari tanpa leading zero5
DNama hari singkat (Inggris)Fri
lNama hari penuh (Inggris)Friday
NHari dalam minggu (1=Senin, 7=Minggu)5
wHari dalam minggu (0=Minggu, 6=Sabtu)5
HJam 24-hour 2 digit14
GJam 24-hour tanpa leading zero14
hJam 12-hour 2 digit02
iMenit 2 digit30
sDetik 2 digit00
AAM atau PMPM
aam atau pmpm
UUnix timestamp1710504000
WNomor minggu dalam tahun (ISO 8601)11
tJumlah hari dalam bulan31
LApakah tahun kabisat (1/0)1
ZOffset timezone dalam detik25200
POffset timezone +HH:MM+07:00
OOffset timezone +HHMM+0700
eNama timezoneAsia/Jakarta
cISO 8601 lengkap2024-03-15T14:30:00+07:00
rRFC 2822Fri, 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-02 karena Februari 2024 hanya 29 hari. Untuk navigasi bulanan yang andal, selalu reset ke hari pertama atau gunakan first 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 #

  • DateTimeImmutable lebih aman dari DateTime — 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 tangkap false sebagai 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.
  • DateInterval dengan notasi ISO 8601P1Y2M3DT4H5M6S untuk durasi kompleks. P1D = 1 hari, PT1H = 1 jam, P1W = 1 minggu.
  • Jebakan +1 month pada akhir bulan menyebabkan overflow ke bulan berikutnya. Gunakan 'first day of next month' untuk navigasi bulanan yang andal.
  • DatePeriod untuk iterasi rentang tanggal — jauh lebih bersih dari loop manual dengan increment hari. Berguna untuk kalender, laporan periodik, dan slot jadwal.
  • diff() mengembalikan DateInterval — gunakan property ->y, ->m, ->d untuk komponen terpisah, atau ->days untuk total hari. Property ->invert bernilai 1 jika tanggal pertama lebih besar.
  • gmdate() atau setTimezone(UTC) untuk mendapatkan waktu UTC dari fungsi prosedural atau sebelum menyimpan ke database.

← Sebelumnya: Array   Berikutnya: Regex →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact