Data tanggal hampir selalu tiba dari luar sebagai string: input formulir 15/01/2026, baris CSV 2026-01-15, sampai respons API berformat ISO. Tugas kita adalah mengubah string menjadi objek waktu (parsing) dan sebaliknya menyusun objek kembali menjadi string (formatting) dengan cara yang terkendali. Konstruktor DateTime menebak formatnya sendiri, dan tebakan tersebut sering meleset — apalagi ketika format Eropa d/m/Y bertabrakan dengan format Amerika m/d/Y. Chapter ini memperkenalkan createFromFormat() untuk parsing yang eksplisit, getLastErrors() untuk mendeteksi tanggal tidak valid, teknik escape pada format(), konstanta format bawaan, serta bahaya ambiguitas d/m/Y yang wajib dikelola sejak awal.

Parsing Terkontrol dengan createFromFormat #

Metode statis createFromFormat($pola, $nilai) membalik logika konstruktor: alih-alih menebak, kita menyatakan format apa yang diharapkan, lalu PHP mencocokkan nilai dengan pola tersebut. Metode ini tersedia di kelas DateTime maupun DateTimeImmutable. Pemisah di dalam pola seperti / dan - harus cocok secara harfiah dengan nilai yang dibaca.

php
<?php
$tanggal = DateTime::createFromFormat("d/m/Y", "15/01/2026");
echo $tanggal->format("d-m-Y l") . "\n";

$waktuLengkap = DateTimeImmutable::createFromFormat("d/m/Y H:i", "15/01/2026 08:30");
echo $waktuLengkap->format("Y-m-d H:i") . "\n";

Output yang Dihasilkan:

text
15-01-2026 Thursday
2026-01-15 08:30

Pola "d/m/Y" berhasil mencocokkan 15/01/2026 karena urutan tanggal, pemisah, bulan, dan tahun persis sama. Tabel berikut merangkum specifier yang paling sering dipakai saat membaca input pengguna.

SpecifierArtiContoh nilai yang cocok
d / jTanggal dua digit / tanpa nol depan05 / 5
m / nBulan dua digit / tanpa nol depan01 / 1
Y / yTahun empat digit / dua digit2026 / 26
H / GJam 24 jam dua digit / tanpa nol depan08 / 8
iMenit dua digit30
sDetik dua digit45
!Mereset bidang lain ke nilai epochdiletakkan di awal pola

Menangkap Kesalahan dengan getLastErrors #

PHP bersikap lunak terhadap tanggal yang mustahil: 2026-02-30 tidak mengeksekusi gagal, melainkan digulirkan maju menjadi 2 Maret. Parser tetap mengembalikan objek, tetapi mencatat sebuah warning yang bisa kita baca lewat metode statis getLastErrors(). Mari kita lihat catatan tersebut.

php
<?php
$tidakValid = DateTime::createFromFormat("Y-m-d", "2026-02-30");

echo $tidakValid->format("Y-m-d") . "\n";
print_r(DateTime::getLastErrors());

Output yang Dihasilkan:

text
2026-03-02
Array
(
    [warning_count] => 1
    [warnings] => Array
        (
            [10] => The parsed date was invalid
        )

    [error_count] => 0
    [errors] => Array
        (
        )

)

Kunci warning_count bernilai 1 dengan pesan The parsed date was invalid, sedangkan error_count bernilai 0. Artinya parser berhasil membaca seluruh string sesuai pola — nilai kalendernya saja yang tidak masuk akal, sehingga tanggal digulirkan menjadi 2026-03-02. Perilaku ini berbeda dengan kegagalan parsing sungguhan ketika nilai tidak cocok dengan pola sama sekali.

php
<?php
$rusak = DateTime::createFromFormat("d/m/Y", "15-01-2026");

var_dump($rusak);
print_r(DateTime::getLastErrors());

Output yang Dihasilkan:

text
bool(false)
Array
(
    [warning_count] => 0
    [warnings] => Array
        (
        )

    [error_count] => 4
    [errors] => Array
        (
            [2] => Unexpected data found.
            [5] => Unexpected data found.
        )

)

Nilai 15-01-2026 memakai pemisah - padahal pola menuntut /, sehingga createFromFormat() mengembalikan false. Kali ini error_count bernilai 4 dengan pesan Unexpected data found. pada array errors. Metode getLastErrors() menyimpan status parsing terakhir secara global, jadi selalu panggil metode tersebut segera setelah satu operasi parsing. Sejak PHP 8.2, metode ini mengembalikan false ketika tidak ada masalah, dan pola berikut merangkai kedua kasus menjadi satu fungsi pemeriksa.

php
<?php
function parseTanggal(string $nilai, string $pola): DateTimeImmutable|false
{
    $tanggal = DateTimeImmutable::createFromFormat($pola, $nilai);
    $errors = DateTimeImmutable::getLastErrors();

    if ($errors !== false && ($errors["warning_count"] > 0 || $errors["error_count"] > 0)) {
        return false;
    }

    return $tanggal;
}

echo "2026-02-28: " . (parseTanggal("2026-02-28", "Y-m-d") === false ? "ditolak" : "diterima") . "\n";
echo "2026-02-30: " . (parseTanggal("2026-02-30", "Y-m-d") === false ? "ditolak" : "diterima") . "\n";

Output yang Dihasilkan:

text
2026-02-28: diterima
2026-02-30: ditolak

Fungsi parseTanggal() menjalankan parsing lalu memeriksa getLastErrors(); gabungan warning maupun error apa pun membuat fungsi mengembalikan false. Tanggal sah 2026-02-28 diterima, sedangkan 2026-02-30 yang akan digulirkan otomatis ditolak lebih dulu. Inilah benteng pertama kita terhadap data kotor dari pengguna.

Escape Karakter Literal pada format() #

Hampir semua huruf latin berperan sebagai karakter format, sehingga teks literal yang mengandung huruf harus di-escape dengan backslash. Tanpa escape, kata seperti “Tanggal” berubah menjadi rangkaian karakter format yang hasilnya membingungkan. Mari kita buktikan perbedaannya.

php
<?php
$waktu = new DateTimeImmutable("2026-01-15 08:00:00", new DateTimeZone("Asia/Jakarta"));

echo $waktu->format("Tanggal: d-m-Y") . "\n";
echo $waktu->format("\\T\\a\\n\\g\\g\\a\\l: d-m-Y") . "\n";

Output yang Dihasilkan:

text
WIBam188amThursday: 15-01-2026
Tanggal: 15-01-2026

Baris pertama memperlihatkan kerusakannya: huruf T terbaca sebagai singkatan zona (WIB), a sebagai penanda pagi (am), n sebagai bulan tanpa nol (1), g sebagai jam 12 jam tanpa nol (8), dan l sebagai nama hari (Thursday), sehingga tercetak WIBam188amThursday: 15-01-2026. Baris kedua memberi awalan backslash pada setiap huruf — dalam string PHP berkutip ganda backslash ditulis ganda seperti \\T — sehingga kata “Tanggal” tercetak apa adanya. Karakter non-huruf seperti -, :, dan / aman tanpa escape.

Konstanta Format Bawaan #

Format standar yang sering dipakai antar sistem sudah disediakan PHP sebagai konstanta kelas, jadi kita tidak perlu menghafal polanya. Tabel berikut merangkum yang paling sering ditemui di dunia nyata.

KonstantaIsi polaBiasa dipakai untuk
DateTime::ATOM (alias W3C, RFC3339)Y-m-d\TH:i:sPAPI berbasis JSON
DateTime::RFC3339_EXTENDEDY-m-d\TH:i:s.vPAPI dengan milidetik
DateTime::RSSD, d M Y H:i:s Ofeed RSS
DateTime::COOKIEl, d-M-Y H:i:s Theader HTTP cookie
php
<?php
$waktu = new DateTimeImmutable("2026-01-15 08:00:00", new DateTimeZone("Asia/Jakarta"));

echo $waktu->format(DateTime::ATOM) . "\n";
echo $waktu->format(DateTime::RFC3339_EXTENDED) . "\n";
echo $waktu->format(DateTime::RSS) . "\n";
echo $waktu->format(DateTime::COOKIE) . "\n";

Output yang Dihasilkan:

text
2026-01-15T08:00:00+07:00
2026-01-15T08:00:00.000+07:00
Thu, 15 Jan 2026 08:00:00 +0700
Thursday, 15-Jan-2026 08:00:00 WIB

Konstanta ATOM menghasilkan format ISO 8601 2026-01-15T08:00:00+07:00 yang aman dipertukarkan antar sistem. Konstanta RFC3339_EXTENDED menambahkan milidetik .000, RSS memakai singkatan hari plus offset numerik +0700, dan COOKIE membuka dengan nama hari lengkap. Bila dua sistem saling bertukar data, pilih salah satu konstanta standar ini alih-alih pola buatan sendiri.

Bahaya Ambiguitas d/m/Y #

String 03/04/2026 menyimpan dua makna sekaligus: 3 April bagi pembaca format d/m/Y, atau 4 Maret bagi pembaca format m/d/Y. PHP sendiri memihak gaya Amerika ketika constructor menebak string berpemisah garis miring. Tiga pembacaan berikut memperlihatkan betapa mudahnya satu nilai yang sama menghasilkan tanggal berbeda.

php
<?php
$ambigu = "03/04/2026";

$bacaDuluTanggal = DateTimeImmutable::createFromFormat("!d/m/Y", $ambigu);
$bacaDuluBulan   = DateTimeImmutable::createFromFormat("!m/d/Y", $ambigu);
$bacaKonstruktor = new DateTimeImmutable($ambigu);

echo "Dibaca d/m/Y : " . $bacaDuluTanggal->format("d F Y") . "\n";
echo "Dibaca m/d/Y : " . $bacaDuluBulan->format("d F Y") . "\n";
echo "Konstruktor  : " . $bacaKonstruktor->format("d F Y") . "\n";

Output yang Dihasilkan:

text
Dibaca d/m/Y : 03 April 2026
Dibaca m/d/Y : 04 March 2026
Konstruktor  : 04 March 2026

Pembacaan d/m/Y menghasilkan 3 April 2026, sedangkan pembacaan m/d/Y menghasilkan 4 Maret 2026. Konstruktor jatuh pada pilihan kedua — gaya Amerika m/d/Y — tanpa memberi peringatan apa pun. Prefix ! pada dua pola pertama memastikan jam dan detik diisi nol sehingga contoh ini dapat direproduksi persis. Satu-satunya pertahanan adalah menetapkan format secara eksplisit di setiap titik masuk data, dan menyimpan tanggal internal dalam format Y-m-d yang tidak ambigu; praktik penyimpanan seperti ini dibahas lebih jauh pada Chapter Menangani Date/Time dengan Aman .

Ringkasan #

createFromFormat() mengubah parsing dari tebakan menjadi deklarasi eksplisit, sementara getLastErrors() membocorkan warning dan error yang senyap sehingga tanggal mustahil seperti 2026-02-30 dapat ditolak lebih awal. Pada arah sebaliknya, format() butuh escape untuk teks literal, dan konstanta seperti DateTime::ATOM menyediakan pola standar siap pakai. Ambiguitas d/m/Y menuntut kita selalu menyatakan format di batas sistem. Setelah momen dan string saling terhubung dengan aman, kita lanjut ke satuan berikutnya: durasi antar momen lewat kelas DateInterval pada Chapter Date Intervals .

Referensi #