Chapter sebelumnya menunjukkan bahwa modify(), setTime(), dan setDate() mengubah objek DateTime di tempat. Perilaku tersebut membuka pintu bug yang khas pada kode berorientasi objek: satu objek dipakai bersama oleh beberapa bagian kode, lalu satu bagian mengubah isinya tanpa sadar bahwa bagian lain juga membacanya. PHP menyediakan kelas kembar bernama DateTimeImmutable yang menutup celah tersebut — setiap operasi pengubahan mengembalikan objek baru sehingga objek lama selalu utuh. Pada chapter ini kita membuktikan jebakan mutasi lewat contoh nyata, melihat cara kerja DateTimeImmutable beserta rantai metodenya, menentukan kapan versi mutable dan immutable layak dipakai, lalu menutup dengan DateTimeInterface sebagai tipe parameter yang menerima keduanya.

Jebakan Objek Mutable #

Penugasan objek di PHP tidak menyalin isi objek, melainkan menyalin penunjuknya: variabel baru dan variabel lama kini mengarah ke satu objek yang sama di memori. Akibatnya, perubahan yang dilakukan lewat satu nama terlihat juga dari nama lainnya. Mari kita buktikan dengan dua variabel yang sama-sama berawal dari jadwal pukul 08:00.

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

$salinan->modify("+1 day");

echo "Pengingat: " . $pengingat->format("Y-m-d H:i:s") . "\n";
echo "Salinan  : " . $salinan->format("Y-m-d H:i:s") . "\n";

Output yang Dihasilkan:

text
Pengingat: 2026-01-16 08:00:00
Salinan  : 2026-01-16 08:00:00

Mari kita telaah apa yang sebenarnya terjadi:

  1. Baris 2: objek DateTime dibuat dengan nilai 2026-01-15 08:00:00 WIB.
  2. Baris 3: penugasan $salinan = $pengingat tidak membuat salinan baru; kedua nama kini menunjuk satu objek yang sama.
  3. Baris 5: modify("+1 day") menggeser objek bersama tersebut menjadi 2026-01-16 08:00:00.
  4. Baris 7-8: kedua baris tercetak 2026-01-16 08:00:00 — padahal kita merasa hanya mengubah salinannya.

Dalam aplikasi nyata, pola ini muncul ketika sebuah function menerima objek jadwal lalu memanggil modify() di dalam tubuhnya. Data milik pemanggil ikut berubah tanpa ada satu baris pun di pemanggil yang menuliskan perubahan itu. Semakin panjang rantai pemanggilan, semakin sulit melacak asal perubahan tersebut.

DateTimeImmutable dan Rantai Metode #

DateTimeImmutable memakai API yang sama dengan DateTime, tetapi setiap metode pengubah mengembalikan objek baru dan sama sekali tidak menyentuh objek lama. Pola ini memungkinkan pemanggilan berantai (chaining) layaknya builder, sekaligus menjamin nilai awal tidak pernah berubah. Mari kita hitung waktu tiba dari waktu berangkat.

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

$tiba = $berangkat->modify("+2 hours")->modify("+30 minutes");

echo "Berangkat: " . $berangkat->format("Y-m-d H:i:s") . "\n";
echo "Tiba     : " . $tiba->format("Y-m-d H:i:s") . "\n";

Output yang Dihasilkan:

text
Berangkat: 2026-01-15 08:00:00
Tiba     : 2026-01-15 10:30:00

Mari kita bedah rantai pada baris keempat:

  1. Baris 4 (modify("+2 hours")): metode ini tidak mengubah $berangkat; metode tersebut mengembalikan objek baru berisi 10:00:00, lalu objek perantara itu langsung dipakai panggilan berikutnya.
  2. Baris 4 (modify("+30 minutes")): panggilan kedua beroperasi pada objek perantara dan mengembalikan satu objek baru lagi berisi 10:30:00 yang akhirnya disimpan ke $tiba.
  3. Baris 6-7: $berangkat tetap tercetak 08:00:00 karena objek aslinya tidak pernah disentuh; hanya $tiba yang memuat 10:30:00.

Metode setTime() dan setDate() mengikuti perilaku yang sama, sehingga keduanya juga aman dirantai.

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

$disesuaikan = $jadwal->setTime(12, 0)->setDate(2026, 6, 1);

echo "Jadwal      : " . $jadwal->format("Y-m-d H:i:s") . "\n";
echo "Disesuaikan : " . $disesuaikan->format("Y-m-d H:i:s") . "\n";

Output yang Dihasilkan:

text
Jadwal      : 2026-01-15 08:00:00
Disesuaikan : 2026-06-01 12:00:00

Panggilan setTime(12, 0) menghasilkan objek baru bertanggal 15 Januari pukul 12:00, lalu setDate(2026, 6, 1) menghasilkan objek baru lagi bertanggal 1 Juni pukul 12:00. Variabel $jadwal tetap menyimpan 2026-01-15 08:00:00 dari awal sampai akhir. Perhatikan pula bahwa kita tidak perlu variabel sementara untuk tiap tahap — rantai metode menanganinya sekaligus.

Kapan Memakai DateTime, Kapan DateTimeImmutable #

Kedua kelas sah dipakai; pilihan bergantung pada seberapa jauh objek menjelajah di dalam kode. Tabel berikut merangkum pertimbangan utamanya.

AspekDateTimeDateTimeImmutable
modify(), setTime(), setDate()Mengubah objek di tempatMengembalikan objek baru
Objek asli setelah dipakai function lainBisa berubah diam-diamDipastikan tetap utuh
Cocok untukAkumulasi nilai dalam satu blok loop kecilValue object, jadwal, data domain
Jejak memoriHemat karena tanpa objek baruMembuat objek baru setiap perubahan

DateTime masih tepat dipakai ketika objek hidup hanya di dalam satu blok kecil dan tidak pernah meninggalkan blok tersebut, misalnya loop yang berjalan hari demi hari untuk mencetak kalender. Begitu objek dilewatkan ke function, disimpan ke array konfigurasi, atau masuk ke cache, mutasi diam-diam menjadi ancaman nyata. Pada titik itu, DateTimeImmutable memberi jaminan yang tidak bisa diberikan DateTime.

DateTimeInterface sebagai Kontrak #

Kedua kelas mengimplementasikan satu interface yang sama: DateTimeInterface. Interface tersebut mendeklarasikan metode baca seperti format(), getTimestamp(), diff(), dan getTimezone() yang tersedia di kedua kelas. Dengan mendeklarasikan parameter bertipe DateTimeInterface, sebuah function dapat menerima DateTime maupun DateTimeImmutable tanpa menduplikasi kode.

php
<?php
function formatJadwal(DateTimeInterface $jadwal, string $label): string
{
    return $label . ": " . $jadwal->format("d-m-Y H:i T");
}

$mutable   = new DateTime("2026-01-15 08:00:00", new DateTimeZone("Asia/Jakarta"));
$immutable = new DateTimeImmutable("2026-01-15 08:00:00", new DateTimeZone("Asia/Jakarta"));

echo formatJadwal($mutable, "Objek DateTime") . "\n";
echo formatJadwal($immutable, "Objek DateTimeImmutable") . "\n";

Output yang Dihasilkan:

text
Objek DateTime: 15-01-2026 08:00 WIB
Objek DateTimeImmutable: 15-01-2026 08:00 WIB

Fungsi formatJadwal() hanya membaca nilai lewat format(), sehingga function tersebut aman menerima objek jenis apa pun. Untuk function yang perlu menghasilkan waktu baru dari masukannya, deklarasikan parameter sekaligus nilai kembalian sebagai DateTimeImmutable agar fungsi tersebut tidak mungkin mengubah data milik pemanggil.

php
<?php
function tambahJam(DateTimeImmutable $jadwal, int $jam): DateTimeImmutable
{
    return $jadwal->modify("+$jam hours");
}

$mula = new DateTimeImmutable("2026-01-15 08:00:00", new DateTimeZone("Asia/Jakarta"));
$selesai = tambahJam($mula, 3);

echo "Mulai   : " . $mula->format("Y-m-d H:i:s") . "\n";
echo "Selesai : " . $selesai->format("Y-m-d H:i:s") . "\n";

Output yang Dihasilkan:

text
Mulai   : 2026-01-15 08:00:00
Selesai : 2026-01-15 11:00:00

Interpolasi " +$jam hours " pada baris keempat menghasilkan string +3 hours yang kemudian diproses modify(). Nilai kembalian tambahJam() adalah objek baru berisi 11:00:00, sementara $mula tetap 08:00:00. Fungsi semacam ini disebut murni: hasilnya hanya ditentukan oleh argumen, tanpa efek samping pada data pemanggil.

Ringkasan #

DateTime mengubah nilai di tempat sehingga dua variabel yang menunjuk objek sama saling memengaruhi — jebakan klasik objek mutable. DateTimeImmutable menutup jebakan itu dengan mengembalikan objek baru pada setiap modify(), setTime(), dan setDate(), memungkinkan rantai metode yang aman. Jadikan versi immutable sebagai default, dan gunakan DateTimeInterface agar function kita netral terhadap kedua jenis objek. Dengan objek yang aman, kita siap menghadapi tantangan berikutnya: mengubah string dari dunia luar menjadi objek waktu dan sebaliknya, topik Chapter Parsing dan Formatting Dates .

Referensi #