Setelah parameter dan return value, property adalah tempat ketiga tempat type declaration memberi manfaat terbesar. Property bertipe (typed properties) hadir sejak PHP 7.4 dan menjadikan class sebagai kontrak yang jelas: setiap property hanya boleh menyimpan satu jenis nilai sejak dideklarasikan. Namun property memiliki satu perilaku khas yang tidak dimiliki parameter — keadaan uninitialized — yang wajib dipahami agar program tidak menabrak Error fatal di produksi. Chapter ini membahas cara mendeklarasikan property bertipe, mengelola keadaan uninitialized, kombinasi dengan readonly, aturan invariance saat inheritance, hingga typed class constants yang diperkenalkan PHP 8.3.

Mendeklarasikan Property Bertipe #

Cara mendeklarasikan property bertipe sama seperti pada parameter: tulis nama tipe sebelum nama variabel property. Property tanpa tipe menerima nilai apa pun, sedangkan property bertipe memvalidasi setiap nilai yang ditugaskan ke property tersebut saat penugasan terjadi. Perhatikan contoh berikut:

php
<?php

class Produk
{
    public string $nama;
    public int $harga;
    public ?string $labelDiskon = null;

    public function ringkas(): string
    {
        $label = $this->labelDiskon ?? "-";
        return "{$this->nama} - Rp{$this->harga} (diskon: {$label})";
    }
}

$produk = new Produk();
$produk->nama = "Kopi Gayo";
$produk->harga = 85000;

echo $produk->ringkas(), "\n";

Output yang Dihasilkan:

text
Kopi Gayo - Rp85000 (diskon: -)

Mari kita telaah deklarasi property pada class Produk di atas:

  1. public string $nama; dan public int $harga;: property bertipe tanpa nilai awal, sehingga keduanya berada dalam keadaan uninitialized sampai diberi nilai dari luar.
  2. public ?string $labelDiskon = null;: property bertipe nullable dengan nilai awal null, sehingga aman dibaca sejak objek dibuat. Notasi ?string adalah bentuk singkat dari string|null yang akan kita bedah tuntas pada Chapter Union, Intersection, dan Nullable Types .
  3. $this->labelDiskon ?? "-": operator null coalescing menggantikan null dengan tanda - agar output tetap rapi.
  4. $produk->nama = "Kopi Gayo";: penugasan nilai string dan integer yang sesuai dengan tipe masing-masing property.

Keadaan Uninitialized — Perbedaan Kunci dengan Parameter #

Parameter dan property menangani “nilai yang belum ada” dengan cara yang berbeda. Parameter yang tidak diisi memicu ArgumentCountError pada saat pemanggilan function, sehingga masalah langsung ketahuan. Sebaliknya, PHP mengizinkan objek dibuat meskipun property bertipe belum diisi — property tersebut berada dalam keadaan uninitialized, dan keadaan ini berbeda dari null.

Membaca property uninitialized langsung memicu Error fatal:

php
<?php

class Pesanan
{
    public DateTimeImmutable $dibuatPada;
}

$pesanan = new Pesanan();
echo $pesanan->dibuatPada->format("Y-m-d"), "\n";

Output yang Dihasilkan:

text

Fatal error: Uncaught Error: Typed property Pesanan::$dibuatPada must not be accessed before initialization in /home/z/my-project/php-course/content/php/type-system-dan-static-analysis/property-types/.verify_tmp.php:9
Stack trace:
#0 {main}
  thrown in /home/z/my-project/php-course/content/php/type-system-dan-static-analysis/property-types/.verify_tmp.php on line 9

Pesan Typed property Pesanan::$dibuatPada must not be accessed before initialization menegaskan bahwa masalahnya bukan nilai null, melainkan property yang belum pernah diisi. PHP membedakan dua keadaan ini secara ketat pada property bertipe. Untuk mengecek keadaan tersebut tanpa memicu error, gunakan isset():

php
<?php

class Pesanan
{
    public string $nomor = "BELUM-DIBUAT";
    public DateTimeImmutable $dibuatPada;
}

$pesanan = new Pesanan();
var_dump(isset($pesanan->nomor));
var_dump(isset($pesanan->dibuatPada));

Output yang Dihasilkan:

text
bool(true)
bool(false)

Hasil isset($pesanan->dibuatPada) bernilai false karena property tersebut belum diisi, sementara isset($pesanan->nomor) bernilai true karena property memiliki nilai awal. Tabel berikut merangkum perbedaan perilaku antara property tanpa tipe dan property bertipe:

SituasiProperty tanpa tipeProperty bertipe
Nilai bawaan saat instansiasinulluninitialized
Dibaca sebelum diisinull, tanpa errorError fatal
Hasil isset() sebelum diisifalsefalse

Penegakan Tipe Saat Penugasan #

Validasi tipe property berlangsung terus-menerus selama umur objek, bukan hanya saat konstruktor berjalan. Setiap penugasan nilai yang tidak cocok dengan deklarasi memicu TypeError fatal:

php
<?php

class Produk
{
    public int $stok;
}

$produk = new Produk();
$produk->stok = "banyak";
echo $produk->stok, "\n";

Output yang Dihasilkan:

text

Fatal error: Uncaught TypeError: Cannot assign string to property Produk::$stok of type int in /home/z/my-project/php-course/content/php/type-system-dan-static-analysis/property-types/.verify_tmp.php:9
Stack trace:
#0 {main}
  thrown in /home/z/my-project/php-course/content/php/type-system-dan-static-analysis/property-types/.verify_tmp.php on line 9

Interpreter menolak string "banyak" karena property $stok hanya menerima int. Perlu dicatat bahwa string numerik seperti "12" masih akan dikonversi secara coercive pada mode default — mekanisme koersi ini mengikuti aturan yang sama dengan parameter sebagaimana dibahas pada Chapter Parameter Types .

Konstruktor Promoted dan readonly #

Kombinasi yang paling sering dipakai di kode modern adalah constructor property promotion, type declaration, dan modifier readonly. Type declaration menjamin jenis nilai yang masuk, sedangkan readonly menjamin property tidak dapat diubah setelah inisialisasi selesai:

php
<?php

class Kredensial
{
    public function __construct(
        public readonly string $email,
        public readonly string $token,
    ) {
    }
}

$kredensial = new Kredensial("[email protected]", "YOUR_API_KEY");
echo "{$kredensial->email} memakai token {$kredensial->token}\n";

Output yang Dihasilkan:

text
[email protected] memakai token YOUR_API_KEY

Karena kedua parameter dipromosikan dengan public readonly string, class Kredensial tidak membutuhkan body constructor tambahan sama sekali. Objek langsung lahir terisi penuh sehingga keadaan uninitialized tidak mungkin terjadi. Pembahasan mendalam tentang readonly tersedia pada Chapter Readonly Properties .

Property Types Bersifat Invariant #

Saat sebuah class mewarisi class lain, tipe property tidak boleh diubah sama sekali — bahkan perubahan yang tampak “lebih longgar” seperti menambah ? sekalipun langsung ditolak. Perhatikan contoh berikut:

php
<?php

class Produk
{
    public string $nama;
}

class Buku extends Produk
{
    public ?string $nama;
}

Output yang Dihasilkan:

text

Fatal error: Type of Buku::$nama must be string (as in class Produk) in /home/z/my-project/php-course/content/php/type-system-dan-static-analysis/property-types/.verify_tmp.php on line 8

PHP menghentikan program dengan pesan Type of Buku::$nama must be string (as in class Produk) bahkan sebelum satu baris logika pun dijalankan, karena pemeriksaan ini terjadi saat class dideklarasikan. Alasannya: property dapat dibaca dan ditulis melalui referensi bertipe parent. Jika class anak boleh memperluas tipe, kode yang membaca property lewat parent bisa menerima nilai di luar jaminan; jika boleh menyempitkan, kode yang menulis lewat parent bisa terhalang. Karena arah baca dan tulis berlawanan, satu-satunya pilihan aman adalah tipe yang identik — property bersifat invariant. Perbandingan lengkap dengan parameter dan return value dibedah pada Chapter Variance .

Typed Class Constants #

PHP 8.3 memperluas type declaration ke konstanta class. Sintaksnya sama: tulis tipe sebelum kata kunci const, dan aturan ini berlaku juga untuk konstanta pada interface maupun enum:

php
<?php

class Konfigurasi
{
    public const string NAMA_TOKO = "Toko Budi";
    public const int UKURAN_HALAMAN = 20;
    public const bool MODE_DEBUG = false;
}

interface Platform
{
    public const string KANAL = "web";
}

echo Konfigurasi::NAMA_TOKO, "\n";
echo Konfigurasi::UKURAN_HALAMAN, "\n";
var_dump(Konfigurasi::MODE_DEBUG);
echo Platform::KANAL, "\n";

Output yang Dihasilkan:

text
Toko Budi
20
bool(false)
web

Setiap konstanta kini terkunci pada satu tipe: NAMA_TOKO harus string, UKURAN_HALAMAN harus int, dan MODE_DEBUG harus bool. Nilai yang melanggar deklarasi ditolak sebelum eksekusi dimulai:

php
<?php

class Konfigurasi
{
    public const int BATAS = "sepuluh";
}

Output yang Dihasilkan:

text

Fatal error: Cannot use string as value for class constant Konfigurasi::BATAS of type int in /home/z/my-project/php-course/content/php/type-system-dan-static-analysis/property-types/.verify_tmp.php on line 5

Error Cannot use string as value for class constant Konfigurasi::BATAS of type int muncul pada saat kompilasi, sehingga konstanta dengan nilai salah tidak akan pernah sempat dipakai. Rincian fitur lain dari PHP 8.3 dapat dibaca pada panduan migrasi resmi .

Ringkasan #

Property bertipe menjadikan class sebagai kontrak yang ditegakkan runtime: nilai yang tidak cocok ditolak dengan TypeError, konstanta class terkunci tipenya sejak PHP 8.3, dan kombinasi dengan readonly menghasilkan objek yang aman dari mutasi. Keadaan uninitialized adalah satu-satunya jebakan besar — selalu isi property di constructor atau periksa dengan isset() sebelum membaca. Notasi ?string yang kita pakai berulang kali di atas sebenarnya adalah bentuk singkat dari union type dengan null; Chapter berikutnya membedah tuntas seluruh tata bahasa union, intersection, nullable, hingga DNF types.