Constructor mengajari kita kapan data diisi; Chapter ini mengangkat pertanyaan yang lebih mendasar: dalam kondisi apa sebuah object boleh dianggap jadi. Program yang baik menjaga setiap object berada dalam kondisi valid sejak detik pertama diciptakan — bukan “nanti diisi juga” atau “semoga pemanggil ingat”. Kita akan membedah urutan inisialisasi di balik layar, galat khas property yang belum terisi, lalu beberapa pola untuk menangani state yang memang mahal atau opsional: lazy initialization, null object, readonly, dan factory function.

Urutan Inisialisasi Object #

Saat new dieksekusi, PHP menjalankan tiga tahap berurutan: menciptakan object kosong, memasang nilai default semua property (termasuk promoted parameter dari constructor), lalu menjalankan badan constructor. Kita bisa membuktikan urutan tersebut dengan mengintip dari dalam constructor:

php
<?php
class Tugas
{
    public string $status = "baru";

    public function __construct(public string $judul)
    {
        var_dump($this->judul, $this->status);
        $this->status = "siap";
    }
}

$tugas = new Tugas("Belajar OOP");
echo "status akhir: {$tugas->status}\n";

Output yang Dihasilkan:

text
string(11) "Belajar OOP"
string(4) "baru"
status akhir: siap

Baris 4 memberi $status nilai default "baru", sedangkan baris 6 mempromosikan parameter $judul menjadi property. Kedua nilai itu sudah terpasang saat baris 8 berjalan — var_dump() menampilkan $this->judul berisi "Belajar OOP" dan $this->status masih "baru" — karena keduanya dipasang pada tahap dua, sebelum badan constructor. Baris 9 baru kemudian mengubah $status, dan baris 14 mengonfirmasi hasil akhirnya. Implikasinya penting: badan constructor selalu bekerja pada object yang semua property-nya sudah berstatus terisi, jadi kita tidak perlu (dan tidak bisa) mengatur ulang urutan tersebut.

Object yang Valid Sejak Lahir #

Prinsip valid from birth berarti: bila sebuah data wajib ada agar object bermakna, jadikan data itu syarat penciptaan. Mari kita lihat gejala pelanggarannya terlebih dahulu — property nullable yang “menunggu diisi suatu hari”:

php
<?php
class Pesanan
{
    public ?string $nomor = null;
    public ?int $total = null;

    public function ringkas(): string
    {
        return "Pesanan #{$this->nomor} - Rp{$this->total}";
    }
}

$pesanan = new Pesanan();
echo $pesanan->ringkas() . "\n";

Output yang Dihasilkan:

text
Pesanan # - Rp

Object $pesanan berhasil diciptakan tanpa perlawanan, tetapi ringkas() menghasilkan Pesanan # - Rp — dua data inti kosong melompong. Dalam program nyata, pesanan tanpa nomor bisa lolos sampai lapisan cetak struk sebelum akhirnya terdeteksi. Sekarang bandingkan dengan versi yang menjadikan data wajib sebagai kontrak penciptaan:

php
<?php
class Pesanan
{
    public function __construct(
        public string $nomor,
        public int $total,
    ) {
    }

    public function ringkas(): string
    {
        return "Pesanan #{$this->nomor} - Rp{$this->total}";
    }
}

$pesanan = new Pesanan("PSN-001", 150000);
echo $pesanan->ringkas() . "\n";

Output yang Dihasilkan:

text
Pesanan #PSN-001 - Rp150000

Pada versi kedua, Pesanan tanpa nomor dan total tidak mungkin ada — PHP menolaknya lewat ArgumentCountError sebelum object sempat lahir. Nullable memang tetap sah dipakai, tetapi hanya untuk data yang memang boleh kosong (misalnya tanggal pembatalan yang baru terisi bila pembeli membatalkan). Pertanyaan yang layak diajukan pada setiap property: “apakah object ini masih bermakna bila nilainya null?” Bila jawabannya tidak, pindahkan data tersebut ke constructor.

Property Terketik yang Belum Diisi #

Ada satu kondisi antara “terisi” dan “bernilai null”: property bertipe yang dideklarasikan tanpa default dan belum pernah diisi. PHP menandainya sebagai belum terinisialisasi dan menolak pembacaannya:

php
<?php
class Produk
{
    public int $harga;
}

$produk = new Produk();

try {
    echo $produk->harga;
} catch (Error $e) {
    echo "Galat: {$e->getMessage()}\n";
}

Output yang Dihasilkan:

text
Galat: Typed property Produk::$harga must not be accessed before initialization

Property $harga pada baris 4 tidak memiliki default sehingga menunggu diisi. Baris 10 mencoba membacanya sebelum sempat ditulis, dan PHP melempar Error dengan pesan Typed property ... must not be accessed before initialization — kita menangkapnya dengan try/catch agar program bisa menampilkan pesan yang rapi alih-alih berhenti total (mekanisme ini sudah kita kenal di Section Error Handling ). Galat keras seperti ini sebenarnya berita baik: kesalahan tertangkap di titik kejadian.

Inisialisasi Malas #

Tidak semua data layak disiapkan di constructor. Laporan yang butuh hitungan berat atau data yang hanya kadang dipakai lebih baik ditunda sampai benar-benar diperlukan — pola ini disebut lazy initialization:

php
<?php
class LaporanService
{
    public ?array $cache = null;

    public function dataPenjualan(): array
    {
        if ($this->cache === null) {
            echo "(menghitung laporan...)\n";
            $this->cache = ["januari" => 150, "februari" => 200, "maret" => 175];
        }

        return $this->cache;
    }
}

$laporan = new LaporanService();

$totalPertama = array_sum($laporan->dataPenjualan());
$totalKedua = array_sum($laporan->dataPenjualan());

echo "Total: $totalPertama, lalu $totalKedua\n";

Output yang Dihasilkan:

text
(menghitung laporan...)
Total: 525, lalu 525

Baris 8 memeriksa penanda $cache === null: hanya pada pemanggilan pertama perhitungan berjalan (disimulasikan echo di baris 9 dan array di baris 10), setelah itu hasilnya disimpan di property. Pemanggilan kedua di baris 20 menemukan cache terisi sehingga langsung mengembalikan data tanpa menghitung ulang — itulah mengapa baris (menghitung laporan...) hanya muncul sekali padahal method dipanggil dua kali. Syarat pola ini: data harus boleh dikatakan “belum ada” di awal, sehingga null sebagai penanda sah dipakai di sini, berbeda dengan data wajib pada bagian sebelumnya.

Null Object sebagai Pengganti yang Diam #

Pola lain untuk state opsional adalah null object: alih-alih memeriksa null di setiap titik pemakaian, kita menyediakan object pengganti yang method-nya sengaja tidak melakukan apa pun:

php
<?php
class LoggerConsole
{
    public function catat(string $pesan): void
    {
        echo "[LOG] $pesan\n";
    }
}

class LoggerNull
{
    public function catat(string $pesan): void
    {
        // sengaja tidak melakukan apa pun
    }
}

class PesananService
{
    public function __construct(public object $logger)
    {
    }

    public function proses(string $nomor): void
    {
        $this->logger->catat("Memproses pesanan $nomor");
        echo "Pesanan $nomor selesai diproses\n";
    }
}

$riuh = new PesananService(new LoggerConsole());
$riuh->proses("PSN-041");

$senyap = new PesananService(new LoggerNull());
$senyap->proses("PSN-042");

Output yang Dihasilkan:

text
[LOG] Memproses pesanan PSN-041
Pesanan PSN-041 selesai diproses
Pesanan PSN-042 selesai diproses

PesananService::proses() menulis log tanpa peduli logger yang dipakai riuh atau senyap. Object LoggerNull menelan seluruh panggilan tanpa efek, sehingga pesanan PSN-042 tetap diproses hanya tanpa baris [LOG]. Manfaatnya: kode pemakai bebas dari pemeriksaan if ($logger !== null) yang berulang. Perlu jujur diakui, tipe object pada baris 20 masih longgar — bentuk yang lebih ketat membutuhkan interface, yang akan kita pelajari pada Chapter Interfaces.

readonly Property sebagai Preview #

Beberapa data memang tidak boleh berubah setelah penciptaan — lingkungan aplikasi atau versi API misalnya. PHP 8.1 menyediakan kata kunci readonly untuk property yang hanya boleh ditulis sekali, yaitu dari dalam constructor:

php
<?php
class Konfigurasi
{
    public function __construct(
        public readonly string $lingkungan,
        public readonly string $versiApi,
    ) {
    }

    public function label(): string
    {
        return "API {$this->versiApi} di lingkungan {$this->lingkungan}";
    }
}

$konfig = new Konfigurasi("produksi", "2.1");
echo $konfig->label() . "\n";

Output yang Dihasilkan:

text
API 2.1 di lingkungan produksi

Dua property di baris 5-6 terisi sekali lewat promotion, lalu terkunci. Mencoba menimpa $konfig->lingkungan dari luar constructor memicu Error — PHP menolak menulis ulang property readonly. Penanda ini cocok untuk nilai identitas yang seharusnya kekal sepanjang umur object, dan kita akan bertemu lagi bentuk-bentuk lanjutannya pada Section Advanced Object Model.

Factory Function dan Konstruksi Ulang #

Ketika proses penciptaan object butuh keputusan atau langkah tambahan, pindahkan logika itu ke sebuah factory function — function biasa yang tugasnya menyiapkan dan mengembalikan object yang siap:

php
<?php
class KoneksiDb
{
    public function __construct(public string $host, public string $basisData)
    {
    }
}

function buatKoneksiDefault(): KoneksiDb
{
    return new KoneksiDb("127.0.0.1", "toko_online");
}

$koneksi = buatKoneksiDefault();
echo "Terhubung ke {$koneksi->basisData} di {$koneksi->host}\n";

Output yang Dihasilkan:

text
Terhubung ke toko_online di 127.0.0.1

Pemanggil tidak perlu tahu host default apa yang dipakai; ia hanya menerima object KoneksiDb yang sudah benar. Factory juga tempat nyaman untuk memilih salah satu dari beberapa class, membaca konfigurasi, atau memvalidasi masukan — semua tanpa menumpuk pekerjaan di dalam constructor itu sendiri. Pola serupa berguna saat kita butuh “salinan dengan perubahan”:

php
<?php
class Produk
{
    public function __construct(
        public string $nama,
        public int $harga,
    ) {
    }
}

function buatVersiDiskon(Produk $sumber, int $persen): Produk
{
    $baru = new Produk($sumber->nama, $sumber->harga);
    $baru->harga = (int) round($baru->harga * (100 - $persen) / 100);

    return $baru;
}

$asli = new Produk("Kopi Gayo", 85000);
$diskon = buatVersiDiskon($asli, 10);

echo "Harga asli: Rp{$asli->harga}\n";
echo "Harga diskon: Rp{$diskon->harga}\n";

Output yang Dihasilkan:

text
Harga asli: Rp85000
Harga diskon: Rp76500

buatVersiDiskon() menciptakan object baru lewat constructor (baris 13) alih-alih menyalin object lama, lalu menyesuaikan harganya. Hasilnya dua object yang saling bebas: $asli tetap Rp85000, $diskon Rp76500. PHP memang punya operator clone untuk menyalin object, tetapi menyalin melewati constructor dan membawa serta seluruh property lama apa adanya — kaki yang sering terpeleset. Kami tunda pembahasan clone beserta jebakannya ke Chapter Cloning Objects; untuk sekarang, biasakan membangun object baru lewat constructor.

Ringkasan #

Inisialisasi object berjalan tiga tahap — object kosong, nilai default dan promoted property, lalu badan constructor — dan prinsip yang menjaganya adalah menjadikan data wajib sebagai syarat penciptaan. Property bertipe tanpa default menolak dibaca sebelum diisi, sementara data yang mahal atau opsional ditangani lewat lazy initialization dan null object. Property readonly mengunci nilai identitas, dan factory function memindahkan logika persiapan keluar dari constructor. Satu hal masih terbuka: sejauh ini semua property kita public dan bebas diutak-atik siapa pun — Chapter berikutnya memperkenalkan visibility untuk melindunginya.

Referensi #