PHP property hooks, bir sınıf özelliğinin okunma ve yazılma anına kod bağlamanızı sağlayan PHP 8.4 özelliğidir. Özelliğin sonuna noktalı virgül yerine süslü parantez açar, içine get ve/veya set kancası yazarsınız: public string $ad { set => trim($value); }. Dışarıdan bakıldığında hâlâ sıradan bir özellik vardır ($nesne->ad = ' Ali '), ama atama sırasında sizin kodunuz çalışır. Böylece getAd() ve setAd() metotları yazmadan doğrulama, biçimlendirme ve hesaplanan değerler elde edersiniz.

Bu yazıda kancaların tam ve kısa sözdizimini, değer saklayan (backed) ve saklamayan (virtual) özellik ayrımını, set parametresinin tipini genişletmeyi, kurucu özellik tanıtımıyla (constructor promotion) birlikte kullanımı, dizi özelliklerindeki tuzağı, kalıtımda final kancaları ve parent::$x::set() çağrısını, arayüz ve soyut sınıflarda özellik tanımlamayı anlatıyoruz. Kodların tamamı PHP 8.5.12 ile çalıştırıldı; PHP 8.3 ve öncesinde bu sözdizimi tanınmaz. Serinin tamamı için PHP öğrenme rehberi sayfasına bakabilirsiniz.

Property Hooks Hangi Sorunu Çözer?

Klasik PHP sınıflarında bir özelliği doğrulamak isterseniz onu private yapar, yanına bir okuma ve bir yazma metodu eklerdiniz. Kılavuz, kancaların iki amaca hizmet ettiğini söyler. Birincisi, özellikleri doğrudan public bırakıp ileride gerekirse davranış ekleme kapısını açık tutmak; böylece "belki bir gün doğrulama gerekir" diye baştan yazılan boş getter ve setter metotları gereksizleşir. İkincisi, bir değer saklamadan nesneyi tanımlayan, örneğin ad ve soyaddan üretilen tam ad gibi özellikler yazabilmek.

Bu ikinci nokta önemlidir: bugün public string $ad; olarak tanımladığınız bir özelliğe yarın set kancası eklediğinizde, o sınıfı kullanan kodların hiçbirini değiştirmeniz gerekmez. Çağıran taraf $k->ad = ... yazmaya devam eder. Metot tabanlı tasarımda ise özelliği public bıraktığınız gün bu esnekliği kaybederdiniz. Sınıf ve özellik temelleri için PHP sınıf ve nesne yazısına bakın.

Temel Sözdizimi: get, set ve Kısa Yazımlar

Kancalar yalnızca statik olmayan özelliklerde kullanılabilir ve tipli ya da tipsiz özelliklerle çalışır. Bir özellik için yalnızca get, yalnızca set veya ikisini birden tanımlayabilirsiniz. Aşağıdaki örnek, PHP 8.4 sürüm notlarındaki örneğin Türkçe bir uyarlamasıdır ve üç farklı kullanımı bir arada gösterir.

php
<?php
class Kisi
{
    // Sanal özellik: değer saklamaz, her okumada hesaplanır.
    public string $tamAd {
        get => $this->ad . ' ' . $this->soyad;
    }

    // Kısa set: ifadenin sonucu özelliğe yazılır.
    public string $ad {
        set => mb_convert_case($value, MB_CASE_TITLE, 'UTF-8');
    }

    // Uzun set: değeri kendiniz yazmalısınız.
    public string $soyad {
        set {
            if (mb_strlen($value) < 2) {
                throw new InvalidArgumentException('Soyad çok kısa');
            }
            $this->soyad = mb_strtoupper($value, 'UTF-8');
        }
    }
}

$k = new Kisi();
$k->ad = 'ayşe';
$k->soyad = 'yıldız';
echo $k->ad, "\n";      // çıktı: Ayşe
echo $k->tamAd, "\n";   // çıktı: Ayşe YILDIZ

try {
    $k->soyad = 'Y';
} catch (InvalidArgumentException $e) {
    echo $e->getMessage(), "\n";   // çıktı: Soyad çok kısa
}
echo $k->soyad, "\n";   // çıktı: YILDIZ

$k->tamAd = 'Başka Biri';
// Fatal error: Uncaught Error: Property Kisi::$tamAd is read-only in /yol/ornek.php:38

Bu örnekte kısa yazımların hepsi görülüyor. get tek bir ifadeden oluşuyorsa süslü parantez yerine => kullanılır. set kancasının parametresini yazmazsanız gelen değer otomatik olarak $value adını alır ve tipi özelliğin tipiyle aynı kabul edilir. set => ifade; biçiminde ise ifadenin sonucu doğrudan özelliğin saklanan değerine yazılır. Uzun set { ... } gövdesinde bu otomatik yazma yoktur; değeri $this->soyad = ... ile kendiniz atamanız gerekir. Kısa ve uzun biçimler birbirinden bağımsızdır, istediğiniz gibi karıştırabilirsiniz.

Doğrulama başarısız olduğunda fırlatılan istisna atamayı tamamen iptal eder: $k->soyad önceki değeri olan YILDIZ olarak kalır. Hata fırlatma ve yakalama ayrıntıları için try catch yazısına bakın. Uzun gövdede başka metotları da çağırabilirsiniz; kancalar nesnenin kapsamında çalıştığı için private ve protected metotlara ve özelliklere erişebilir. Başka bir özelliğe erişmek, o özelliğin kendi kancalarını atlamaz.

Backed ve Virtual Özellik Farkı

Backed ve Virtual Özellik Farkı: var_dump(), serialize() ve unserialize(), (array) dönüşümü, get_mangled_object_vars()

Kancalı bir özellik ya değer saklar (backed) ya da saklamaz (virtual). Kural basittir: kancalardan en az biri özelliğin kendisine tam olarak $this->ozellikAdi yazımıyla başvuruyorsa özellik backed'dir. Hiçbiri başvurmuyorsa virtual'dır ve nesnede hiç yer kaplamaz. Kancası olmayan her özellik zaten backed sayılır. Kılavuz, $this->$degisken gibi dolaylı bir başvurunun sayılmadığını özellikle belirtir.

Bu ayrım davranışı doğrudan etkiler. Backed bir özellikte yazmadığınız kanca varsayılan davranışla çalışır: yalnızca set yazdıysanız okuma normal şekilde saklanan değeri döndürür. Virtual bir özellikte ise yazmadığınız işlem hiç yoktur. Yukarıdaki $tamAd yalnızca get içerdiği için yazma denemesi Property Kisi::$tamAd is read-only hatası verdi.

php
<?php
class Dikdortgen
{
    public int $alan {
        get => $this->en * $this->boy;
    }

    public function __construct(public int $en, public int $boy) {}
}

$d = new Dikdortgen(4, 5);
echo $d->alan, "\n";            // çıktı: 20
echo json_encode($d), "\n";     // çıktı: {"alan":20,"en":4,"boy":5}
var_dump($d);
/* çıktı:
object(Dikdortgen)#1 (2) {
  ["en"]=>
  int(4)
  ["boy"]=>
  int(5)
}
*/

Çıktıdaki fark dikkat çekici: json_encode() değeri get kancasından okuduğu için alan anahtarı JSON'da var, ama var_dump() saklanan ham değerleri gösterdiği için virtual alan özelliği dökümde hiç görünmüyor. Kılavuz hangi işlemin kancadan, hangisinin ham değerden geçtiğini tek tek listeler:

İşlemDeğeri nereden alır?
var_dump()Ham değer (kanca çalışmaz)
serialize() ve unserialize()Ham değer
(array) dönüşümüHam değer
get_mangled_object_vars()Ham değer
var_export()get kancası
json_encode()get kancası
get_object_vars()get kancası
JsonSerializable, __serialize() ve __unserialize()Sizin kodunuz; içinde kancalar çalışır

Pratik sonuç şu: bir nesneyi var_dump() ile incelerken virtual özellikleri göremezsiniz ve backed özelliklerde get kancasının dönüştürdüğü değeri değil, saklanan değeri görürsünüz. Hata ayıklarken bu farkı akılda tutmak, "değer neden farklı görünüyor?" sorusunu baştan çözer. JSON tarafının ayrıntıları json_encode yazısında anlatılıyor.

set Parametresinin Tipini Genişletmek

set kancasına parametreyi tipiyle birlikte açıkça yazabilirsiniz. Bu tip ya özelliğin tipiyle aynı olmalı ya da ondan daha geniş (contravariant) olmalıdır. Örneğin float tipli bir özellik, set(float|string $value) ile metin de kabul edip içeride sayıya çevirebilir. Daha dar bir tip yazmak ise derleme hatasıdır. Okuma tarafında özellik her zaman kendi tipini döndürür; genişletme yalnızca atama yönünde geçerlidir.

php
<?php
class Fiyat
{
    public float $tutar {
        set(float|string $value) {
            $this->tutar = is_string($value)
                ? (float) str_replace(',', '.', $value)
                : $value;
        }
    }
}

$f = new Fiyat();
$f->tutar = '12,50';
var_dump($f->tutar);   // çıktı: float(12.5)
$f->tutar = 3;
var_dump($f->tutar);   // çıktı: float(3)

İkinci atamada 3 tam sayısı float olarak saklandı; bu, kancanın değil PHP'nin normal tip dönüşümünün sonucudur. Uyumsuz bir tip yazdığınızda, örneğin string özelliğe set(int $value) tanımladığınızda, PHP 8.5.12 dosyayı çalıştırmaz: Fatal error: Type of parameter $value of hook Y::$a::set must be compatible with property type. Tip uyumluluğu kuralları için tip bildirimleri yazısına bakın.

Kurucu Özellik Tanıtımı ile Kancalar

Kancalar kurucu parametresinde tanıtılan özelliklerde de yazılabilir. Ancak kılavuzun özellikle uyardığı bir ayrıntı var: motor bu yapıyı, özellik tanımı ve kurucu gövdesindeki bir atama olarak ikiye ayırır ve kurucunun parametre tipi olarak set kancasının geniş tipini değil, özelliğin kendi tipini kullanır. Yani kanca metin kabul etse bile kurucu yalnızca özelliğin tipini kabul eder.

php
<?php
class Olay
{
    public function __construct(
        public private(set) DateTimeImmutable $tarih {
            set (string|DateTimeImmutable $value) {
                $this->tarih = is_string($value) ? new DateTimeImmutable($value) : $value;
            }
        },
    ) {}
}

$o = new Olay(new DateTimeImmutable('2026-11-07'));
echo $o->tarih->format('d.m.Y'), "\n";   // çıktı: 07.11.2026

new Olay('2026-11-07');
// Fatal error: Uncaught TypeError: Olay::__construct(): Argument #1 ($tarih) must be of type DateTimeImmutable, string given, called in /yol/ornek.php on line 16 and defined in /yol/ornek.php:5

Bu örnekte public private(set) asimetrik görünürlüğü de kullanıldı: özellik herkes tarafından okunabilir ama yalnızca sınıfın içinden yazılabilir. Dışarıdan atama denemesi Cannot modify private(set) property Olay::$tarih from global scope hatası verir. Kurucuda metin de kabul etmek istiyorsanız kılavuzun önerisi, kurucu tanıtımını bırakıp özelliği sınıf gövdesinde tanımlamak ve kurucuda normal bir atama yapmaktır. Asimetrik görünürlük public, private, protected ve readonly yazısında anlatılıyor; tarih nesneleri için DateTime yazısına bakın.

readonly ile Birlikte Kullanılamaz

readonly ile Birlikte Kullanılamaz: public özellik, private özellik + getX()/setX(), public readonly, public private(set)

Kancalı bir özellik readonly olamaz; PHP 8.5.12 bu denemeyi Fatal error: Hooked properties cannot be readonly ile durdurur. Kılavuzun önerisi, hem davranışı değiştirmek hem de yazma erişimini kısıtlamak istediğinizde asimetrik görünürlük kullanmanızdır: public private(set) string $kod { set => ... }. Aşağıdaki tablo, bir özelliği korumak için kullanılabilecek araçları karşılaştırıyor.

YöntemDışarıdan okumaDışarıdan yazmaAtamada kod çalışır mı?Sürüm
public özellikVarVarHayırHer sürüm
private özellik + getX()/setX()MetotlaMetotlaEvetHer sürüm
public readonlyVarYok, bir kez sınıf içindenHayır8.1+
public private(set)VarYok, sınıf içinden serbestHayır8.4+
set kancasıVarVar, kanca üzerindenEvet8.4+
private(set) + set kancasıVarYok, sınıf içinden kanca üzerindenEvet8.4+

Dizi Özellikleri ve Referans Tuzağı

Kancalar okuma ve yazmayı araya girerek yakaladığı için, özelliğe referans almak veya dolaylı değişiklik yapmak sorun çıkarır. Dizi özelliğinde $nesne->liste[] = 'x' yazmak aslında dizinin referansı üzerinden çalışır ve bu, set kancasını atlamak anlamına gelirdi. Bu yüzden PHP bu tür işlemleri engeller.

php
<?php
class Etiketler
{
    public array $liste = [] {
        set => array_map('trim', $value);
    }
}

$e = new Etiketler();
$e->liste = [' php ', ' enum'];
echo implode('|', $e->liste), "\n";    // çıktı: php|enum

try {
    $e->liste[] = 'yeni';
} catch (Error $x) {
    echo $x->getMessage(), "\n";
}
// çıktı: Indirect modification of Etiketler::$liste is not allowed

// Doğru yol: tüm diziyi yeniden atamak (kanca çalışır)
$e->liste = [...$e->liste, ' yeni '];
echo implode('|', $e->liste), "\n";    // çıktı: php|enum|yeni

Kılavuzun kuralları şöyle özetlenebilir. Dizinin tamamını yeniden atamak her zaman serbesttir ve normal bir atama gibi davranır; sorun yalnızca tek tek elemanlarla çalışırken çıkar. Backed bir dizi özelliğinin elemanlarına yazmaya yalnızca özellik sadece &get kancası tanımlıyorsa izin verilir. get kancasının başına & koymak onu referans döndüren hale getirir; aynı özellikte hem get hem &get yazmak sözdizimi hatasıdır, backed özellikte &get ile set birlikte de kullanılamaz. Kancalı bir özelliği unset() ile silmek de yasaktır: Cannot unset hooked property. Referansların genel davranışı için PHP referanslar yazısına bakın.

Kalıtım: final Kancalar ve parent::$x::set()

Alt sınıf, bir özelliği yeniden tanımlayarak yalnızca değiştirmek istediği kancayı yazabilir; kancalar metotlar gibi birbirinden bağımsız ezilir. Hiç kancası olmayan bir özelliğe alt sınıfta kanca eklemek de mümkündür. Üst sınıfın kancasını veya saklama alanını kullanmak için parent::$x::get() ve parent::$x::set($value) yazılır. Kılavuz, alt sınıfta doğrudan $this->x = $value yazmak yerine bu yolu önerir; böylece üst sınıf ileride kendi set kancasını eklerse alt sınıf onu atlamamış olur.

php
<?php
class Nokta
{
    public int $x = 0;
}

class PozitifNokta extends Nokta
{
    public int $x {
        set {
            if ($value < 0) {
                throw new InvalidArgumentException('x negatif olamaz');
            }
            parent::$x::set($value);
        }
    }
}

$n = new PozitifNokta();
$n->x = 5;
echo $n->x, "\n";            // çıktı: 5

try {
    $n->x = -1;
} catch (InvalidArgumentException $e) {
    echo $e->getMessage(), "\n";   // çıktı: x negatif olamaz
}

$m = new PozitifNokta();
try {
    echo $m->x, "\n";
} catch (Error $e) {
    echo $e->getMessage(), "\n";
}
// çıktı: Typed property PozitifNokta::$x must not be accessed before initialization

Son çıktı sık gözden kaçan bir kuralı gösteriyor: alt sınıf bir özelliğe kanca eklediğinde üst sınıftaki varsayılan değer (= 0) kaldırılır ve yeniden yazılması gerekir. Bu, kancasız özelliklerde de geçerli olan kalıtım davranışıyla tutarlıdır. Kancayı final işaretlerseniz alt sınıf onu ezemez; örneğin final set => strtolower(trim($value)); tanımlı bir özellikte alt sınıfın kendi set kancasını yazması Fatal error: Cannot override final property hook Kullanici::$eposta::set() verir. Özelliğin tamamı final ise alt sınıf onu hiçbir şekilde yeniden tanımlayamaz. Kalıtımın genel kuralları için kalıtım yazısına bakın.

Arayüzde ve Soyut Sınıfta Özellik Tanımlamak

PHP 8.4 ile arayüzler de özellik tanımlayabilir hale geldi. Arayüz yalnızca public erişimle ilgilenir ve özelliğin okunabilir mi ({ get; }), yazılabilir mi ({ set; }) yoksa ikisi birden mi ({ get; set; }) olacağını belirtir. Uygulayan sınıf bu sözleşmeyi farklı yollarla karşılayabilir: sıradan bir public özellikle, yalnızca istenen kancayı içeren virtual bir özellikle veya okuma şartı için readonly bir özellikle. Yazılabilir olması istenen bir özellik ise readonly olamaz.

php
<?php
interface Adli
{
    public string $ad { get; }
    public string $kod { get; set; }
}

class Urun implements Adli
{
    public function __construct(public string $ad, public string $kod) {}
}

class Kategori implements Adli
{
    private string $saklanan = '';

    public string $ad {
        get => 'Kategori: ' . $this->kod;
    }

    public string $kod {
        get => $this->saklanan;
        set => $this->saklanan = strtoupper($value);
    }
}

function goster(Adli $x): void
{
    echo $x->ad, ' / ', $x->kod, "\n";
}

goster(new Urun('Kalem', 'k-1'));   // çıktı: Kalem / k-1
$c = new Kategori();
$c->kod = 'kirtasiye';
goster($c);                          // çıktı: Kategori: KIRTASIYE / KIRTASIYE

Kategori sınıfındaki iki özellik de virtual'dır: hiçbir kanca kendi özelliğine başvurmuyor, değer ayrı bir private alanda tutuluyor. Arayüz yalnızca okunabilir ve yazılabilir bir $kod istediği için bu yeterlidir. Soyut sınıflar da aynı şekilde soyut özellik tanımlayabilir: abstract public float $alan { get; }. Soyut özellik public veya protected olabilir ve en az bir işlemi gövdesiz bırakmak zorundadır; diğer işlem için varsayılan bir uygulama verebilir. Alt sınıf soyut özelliği karşılamazsa PHP 8.5.12 bunu eksik soyut metot gibi raporlar: Class Daire contains 1 abstract method and must therefore be declared abstract or implement the remaining method (Sekil::$alan::get).

Uygulamalı Mini Örnek: KDV Dahil Fiyat ve Stok Kodu

Şimdi öğrendiklerimizi küçük bir ürün sınıfında birleştirelim. Stok kodu her atamada büyük harfe çevrilip boşluklardan arındırılsın, fiyat negatif olamasın ve KDV dahil fiyat saklanmadan hesaplansın. Kancalar sayesinde sınıfı kullanan kod yalnızca özellik okuyup yazıyor; hiçbir getter veya setter metodu yok.

php
<?php
final class Urun
{
    public const KDV = 0.20;

    public string $stokKodu {
        set => strtoupper(str_replace(' ', '', $value));
    }

    public float $fiyat {
        set {
            if ($value < 0) {
                throw new InvalidArgumentException('Fiyat negatif olamaz');
            }
            $this->fiyat = round($value, 2);
        }
    }

    public float $kdvDahil {
        get => round($this->fiyat * (1 + self::KDV), 2);
    }

    public function __construct(string $stokKodu, float $fiyat)
    {
        $this->stokKodu = $stokKodu;   // kurucudaki atama da kancadan geçer
        $this->fiyat = $fiyat;
    }
}

$u = new Urun(' kl 001 ', 149.999);
echo $u->stokKodu, "\n";    // çıktı: KL001
echo $u->fiyat, "\n";       // çıktı: 150
echo $u->kdvDahil, "\n";    // çıktı: 180

$u->fiyat = 99.9;
echo $u->kdvDahil, "\n";    // çıktı: 119.88
echo json_encode($u), "\n"; // çıktı: {"stokKodu":"KL001","fiyat":99.9,"kdvDahil":119.88}

Burada kurucu tanıtımı kullanılmadı; bunun yerine kurucu gövdesinde normal atamalar yapıldı ve bu atamalar da kancalardan geçti. kdvDahil virtual olduğu için nesnede yer kaplamıyor, ama json_encode() çıktısında yer alıyor. Para hesaplarında kayan nokta yuvarlamasının sınırlarını unutmayın; burada round() yalnızca gösterim amaçlı kullanıldı.

Sık Yapılan Hatalar

Sık Yapılan Hatalar: Virtual özelliğe varsayılan değer vermek, Statik özelliğe kanca yazmak, Uzun set gövdesinde atamayı unutmak, Dizi elemanını doğrudan değiştirmek
  • Virtual özelliğe varsayılan değer vermek: public string $x = 'v' { get => 'y'; } geçersizdir: Cannot specify default value for virtual hooked property.
  • Statik özelliğe kanca yazmak: Kancalar yalnızca nesne özelliklerinde çalışır: Cannot declare hooks for static property.
  • Uzun set gövdesinde atamayı unutmak: set { ... } içinde $this->ozellik = ... yazmazsanız değer saklanmaz. Kısa set => biçimi bu atamayı sizin yerinize yapar.
  • Dizi elemanını doğrudan değiştirmek: $n->liste[] = ... kancalı backed dizide hata verir; diziyi bütün olarak yeniden atayın.
  • var_dump çıktısına güvenmek: var_dump() ham değeri gösterir, virtual özellikleri hiç göstermez. Kancadan geçen değeri görmek için özelliği doğrudan okuyun.
  • Alt sınıfta varsayılan değeri unutmak: Alt sınıf kanca eklediğinde üst sınıftaki varsayılan değer düşer; gerekiyorsa yeniden yazın.

Sık Sorulan Sorular

Property hooks hangi PHP sürümünde var?

PHP 8.4 ile geldi. Arayüzlerde ve soyut sınıflarda özellik tanımlama, __PROPERTY__ sihirli sabiti ve asimetrik görünürlük de aynı sürümün parçasıdır. Daha eski bir sürümde çalışan sunucularda bu kodlar çalışmaz.

Getter ve setter metotlarını tamamen bırakmalı mıyım?

Kılavuzun ifadesiyle kancalar çoğu basmakalıp getter ve setter metodunu gereksiz kılar. Özelliği public tanımlayıp gerektiğinde kanca eklemek yeterlidir. Bununla birlikte birden fazla değeri birlikte değiştiren veya yan etkisi büyük olan işlemler için açık adlı bir metot hâlâ daha anlaşılırdır.

__PROPERTY__ ne işe yarar?

Bir kancanın içinde kullanıldığında o kancanın ait olduğu özelliğin adını verir. Ancak $this->{__PROPERTY__} gibi dolaylı bir başvuru özelliği backed yapmaz; backed olması için adın açıkça yazılması gerekir.

Kancalar __get ve __set sihirli metotlarının yerini alır mı?

Kancalar belirli, önceden tanımlı özellikler içindir. Adını önceden bilmediğiniz, dinamik özelliklere erişimi yakalamak için hâlâ sihirli metotlar kullanılır; ayrıntılar sihirli metotlar yazısında.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: PHP öğrenme rehberi · public, private, protected ve readonly · PHP sınıf ve nesne · PHP 8.4 yenilikleri · PHP sihirli metotlar