Ders 06 / 12
Sihirli Yöntemler
__get, __set, __isset, __call ve __toString yöntemleri, okunan bir adın yöntemden çözülmesi ve bu çözümün değişince kullananın uğradığı örtük ve kıran sonuçlar.
Yayın Güncelleme
Bu derste
- __get, __set, __isset, __call ve __toString yöntemlerini tanımlar ve ne zaman çağrıldıklarını gözlemler
- Özellik okumasının bir yönteme çevrildiği durumda iç adın değişmesinin kullananı nasıl etkilediğini ölçer
- __get bilinmeyen adı reddettiğinde aynı değişikliğin kullananı durdurduğunu gösterir
İçindekiler
Statik Üyeler dersinde sabitin değeri değişince kullanan hata almadan kaydı. Sihirli yöntemlerde (magic method) aynı sessizlik daha büyük bir alana yayılır, çünkü bir özelliği okumak ya da bir yöntemi çağırmak artık sınıfın içindeki bir yönteme çevrilebilir. Soru, bu çevirinin iç adı değişince kullananın neyi gördüğüdür.
Sihirli Yöntemler
Sihirli yöntemler adı iki alt çizgiyle başlayan, PHP’nin belirli durumlarda kendisi çağırdığı yöntemlerdir. __get olmayan ya da erişilemeyen bir özellik okunduğunda, __set yazıldığında, __isset isset() ile sorulduğunda, __call olmayan bir yöntem çağrıldığında, __toString nesne metin olarak kullanıldığında çalışır.
<?php // Ayar.php — okuma, yazma, sorma, yontem cagrisi ve metne cevirmeyi yakalayan sinif class Ayar { private array $degerler = []; public function __set(string $ad, mixed $deger): void { $this->degerler[$ad] = $deger; } public function __get(string $ad): mixed { return $this->degerler[$ad] ?? null; } public function __isset(string $ad): bool { return isset($this->degerler[$ad]); } public function __call(string $ad, array $arguman): string { return $ad . '(' . count($arguman) . ')'; } public function __toString(): string { return 'Ayar[' . count($this->degerler) . ']'; } }
<?php // ayar_kullan.php — sihirli yontemler kullananin yazdigi ifadelerden tetiklenir require 'Ayar.php'; $ayar = new Ayar(); $ayar->tema = 'koyu'; echo $ayar->tema, "\n"; var_dump(isset($ayar->tema), isset($ayar->dil)); echo $ayar->kaydet(1, 2), "\n"; echo $ayar, "\n";
koyu bool(true) bool(false) kaydet(2) Ayar[1]
NM22. tema diye bir özellik tanımlı değildi ve kullanan yine de yazıp okuyabildi: __set değeri $degerler dizisine koydu, __get oradan çıkardı. kaydet() diye bir yöntem de yoktu, __call adı ve argüman sayısını geri verdi. Bu, Sınıflar ve Nesneler dersindeki dinamik özellikten farklıdır: orada özellik nesnenin üzerine kuruluyordu ve bir kullanımdan kaldırma uyarısı (E_DEPRECATED) doğuruyordu, burada ise tanımlı bir yöntem karşılıyor.
__unset ve __callStatic
__unset, erişilemeyen bir özellik unset() ile silinmek istendiğinde çalışır; __isset ile birlikte yazılır, çünkü ikisi aynı veri yapısına bakar. __callStatic ise __call’ın statik karşılığıdır: sınıfta olmayan bir yöntem Sinif::ad() biçiminde çağrıldığında devreye girer. İkisi de adı ve argümanları yöntemin gövdesine verir.
<?php // Kayit.php — silmeyi ve statik cagriyi yakalayan sinif class Kayit { private array $veri = ['a' => 1, 'b' => 2]; public function __isset(string $ad): bool { return isset($this->veri[$ad]); } public function __unset(string $ad): void { unset($this->veri[$ad]); } public static function __callStatic(string $ad, array $arguman): string { return 'statik ' . $ad . '(' . count($arguman) . ')'; } }
<?php // kayit_kullan.php — unset ve statik cagri sihirli yontemlere gider require 'Kayit.php'; $kayit = new Kayit(); var_dump(isset($kayit->a)); unset($kayit->a); var_dump(isset($kayit->a)); echo Kayit::bul(7), "\n";
bool(true) bool(false) statik bul(1)
$veri özel olduğu için $kayit->a dışarıdan erişilemezdi; isset ve unset bu yüzden sihirli yöntemlere düştü ve dizideki anahtarı sildi. __unset yazılmasaydı unset($kayit->a) hiçbir şey yapmadan geçerdi.
Sihirli Yöntem Yalnız Erişilemeyen Adda Çalışır
Sihirli okuma yöntemi bir kısayol değildir: PHP önce adı sınıfın gerçek özelliklerinde arar, bulursa yöntemi hiç çağırmaz. __get yalnız ad yoksa ya da çağıran yerden görünmüyorsa çalışır. Bu ayrım, public bir özelliği sonradan sihirli yönteme taşımanın kullananı nasıl etkilediğini belirler.
<?php // Kasa.php — gercek ozellik varken __get calismaz class Kasa { public string $acik = 'a'; private string $gizli = 'g'; public function __get(string $ad): string { return 'sihirli:' . $ad; } }
<?php // kasa_kullan.php — __get yalniz erisilemeyen adda calisir require 'Kasa.php'; $kasa = new Kasa(); echo $kasa->acik, ' ', $kasa->gizli, ' ', $kasa->yok, "\n"; unset($kasa->acik); echo $kasa->acik, "\n";
a sihirli:gizli sihirli:yok sihirli:acik
acik gerçek ve görünür olduğu için sihirli yönteme uğramadı; gizli dışarıdan görünmediği, yok hiç olmadığı için uğradı. acik silindikten sonra ise adı gerçek özellik olarak bulunamadı ve __get devreye girdi. Bu davranış, sihirli yöntemin gerçek özelliğin yerini sessizce alabileceğini gösterir.
__toString ve Stringable
__toString nesne bir metin bağlamına girdiğinde çalışır: echo, birleştirme, metin bekleyen bir fonksiyon parametresi. PHP 8’de bu yöntemi tanımlayan her sınıf, Stringable arayüzünü kendiliğinden gerçekleştirmiş sayılır; arayüz bildirimine gerek yoktur. Yöntemi olmayan bir nesne metin bağlamına girerse Error doğar.
<?php // Etiket.php — __toString ve Stringable class Etiket { public function __construct(private string $ad) { } public function __toString(): string { return '<' . $this->ad . '>'; } } class Susuz { } $etiket = new Etiket('b'); echo $etiket, ' ', strlen($etiket), ' ', $etiket instanceof Stringable ? 'evet' : 'hayir', "\n"; echo 'Metin: ' . $etiket . "\n"; try { echo new Susuz(); } catch (Error $e) { echo get_class($e), "\n"; }
<b> 3 evet Metin: <b> Error
strlen() nesneyi metne çevirip <b> üzerinden 3 saydı. Stringable bildirimi hiçbir yerde yazılmadığı hâlde instanceof doğru çıktı. Sınıfa __toString eklemek, o sınıfın nesnelerini metin bekleyen her yere sokar; bu kolaylık, dönüşümün biçimini, kullananın söz hakkı olmadan, sınıfın tek başına belirlemesi demektir.
__set ve Yazım Hatası
__set her bilinmeyen adı kabul ettiğinde, adı yanlış yazan kod hata almaz. Yanlış yazılmış ad dizide yeni bir anahtar olur; doğru ad ise eski değerinde kalır. Okuma tarafında ?? null bırakılmışsa yanlış adı okumak da sessizce null verir.
<?php // Esnek.php — her adi kabul eden __set ve __get class Esnek { public string $baslik = ''; private array $ek = []; public function __set(string $ad, mixed $deger): void { $this->ek[$ad] = $deger; } public function __get(string $ad): mixed { return $this->ek[$ad] ?? null; } } $esnek = new Esnek(); $esnek->baslik = 'ilk'; $esnek->baslk = 'ikinci'; echo $esnek->baslik, ' ', $esnek->baslk, "\n"; var_dump($esnek->baslikk);
ilk ikinci NULL
baslk yazım hatasıydı ve __set onu sorgusuz kabul etti; baslik ilk değerinde kaldı. baslikk okunduğunda da hata çıkmadı. Sihirli yöntemi yazan taraf kabul edilen adları bir listeyle sınırlamazsa, kullanan yazım denetimini kaybeder.
__call ile Yönlendirme
__call sık kullanılan bir amaca hizmet eder: başka bir nesneye yönlendirmek. Sarmalayan sınıf bilmediği her yöntemi içindeki nesneye aktarır. Kullanan sarmalayıcıyı çağırır, ama aslında içerideki nesnenin yöntemi çalışır; içerideki nesnede de olmayan bir ad ise yine Error doğurur.
<?php // Sarmal.php — bilinmeyen yontemi ic nesneye yonlendiren sinif class Sarmal { public function __construct(private ArrayObject $ic) { } public function __call(string $ad, array $arguman): mixed { return $this->ic->$ad(...$arguman); } } $sarmal = new Sarmal(new ArrayObject([3, 1, 2])); echo $sarmal->count(), "\n"; try { $sarmal->yoktur(); } catch (Error $e) { echo get_class($e), "\n"; }
3 Error
count() sarmalayıcıda tanımlı değildi ve yönlendirme sayesinde ArrayObject tarafından yanıtlandı. Bu yönlendirme, sarmalanan sınıfın yöntemlerini sarmalayıcının bildiriminden gizler; hangi adların çalıştığı kodu okuyarak değil çalıştırarak anlaşılır.
__invoke
__invoke bir nesneyi fonksiyon gibi çağırmaya izin verir. Nesne $nesne(argüman) biçiminde çağrılır ve PHP yöntemin gövdesini çalıştırır. Nesne bu yüzden callable kabul edilir: array_map gibi bir fonksiyona geri çağrı olarak verilebilir. Durum taşıyan bir dönüşüm gerektiğinde, bu özellik nesnenin özelliklerini çağrının yanında götürür.
<?php // Carpan.php — nesneyi islev gibi cagirmak class Carpan { public function __construct(private int $katsayi) { } public function __invoke(int $sayi): int { return $sayi * $this->katsayi; } } $uc = new Carpan(3); echo $uc(4), ' ', implode(',', array_map($uc, [1, 2, 3])), ' ', var_export(is_callable($uc), true), "\n";
12 3,6,9 true
$uc(4) doğrudan __invoke(4) oldu; array_map nesneyi dizinin her öğesi için çağırdı. Katsayı nesnede durduğu için dönüşümü farklı katsayıyla yeniden kullanmak, yeni bir nesne yaratmaktan ibaret kaldı.
Ölçüm Çekirdeği
Bu dersin ölçümü de yayilim.php dosyasını kullanır; çekirdek ilk derste satır satır açıklanmıştı.
<?php // yayilim.php — bir degisikligin nereye ulastigini olcen yardimcilar function yaz(string $dizin, array $dosyalar): void { foreach ($dosyalar as $ad => $icerik) { if ($icerik === null) { continue; // null: dosya silindi } $yol = "$dizin/$ad"; if (!is_dir(dirname($yol))) { mkdir(dirname($yol), 0777, true); } file_put_contents($yol, $icerik); } } function oku(string $dizin): array { $dosyalar = []; $gezgin = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($dizin, FilesystemIterator::SKIP_DOTS) ); foreach ($gezgin as $dosya) { $ad = substr($dosya->getPathname(), strlen($dizin) + 1); $dosyalar[$ad] = file_get_contents($dosya->getPathname()); } return $dosyalar; } function kostur(string $dizin): array { $komut = 'cd ' . escapeshellarg($dizin) . ' && php -d display_errors=0 main.php 2>/dev/null'; exec($komut, $satirlar, $kod); return [$kod, implode("\n", $satirlar)]; } function olc(string $ad, array $taban, array $degisiklik, array $baglanti = []): string { if (array_key_exists('main.php', $degisiklik)) { throw new LogicException("$ad: kullanan duzenlenmez"); } yaz("olcum/$ad/once", $taban); yaz("olcum/$ad/sonra", array_merge($taban, $degisiklik)); [$kodOnce, $ciktiOnce] = kostur("olcum/$ad/once"); [$kodSonra, $ciktiSonra] = kostur("olcum/$ad/sonra"); if ($kodOnce !== 0) { throw new LogicException("$ad: taban calismadi"); } $sinif = match (true) { $kodSonra !== 0 => 'kiran', $ciktiOnce !== $ciktiSonra => 'ortuk', array_intersect(array_keys($degisiklik), $baglanti) !== [] => 'baglantida', default => 'yerinde', }; printf( "%-18s %d dosya cikis=%-3d cikti %-6s -> %s\n", $ad, count($degisiklik), $kodSonra, $ciktiOnce === $ciktiSonra ? 'ayni' : 'farkli', $sinif ); return $sinif; }
İç Ad Değişir
Ölçülen değişiklik, Ayar sınıfının iç dizisindeki tema anahtarını gorunum olarak yeniden adlandırmaktır. __get ve sınıfın başka hiçbir yeri değişmez. Kullanan main.php (new Ayar())->tema değerini var_dump ile yazdırır.
<?php // sihirli_okuma.php — __get'in baktigi ic anahtar yeniden adlandirilir require_once 'yayilim.php'; olc('sihirli-okuma', [ 'ayar.php' => <<<'PHP' <?php class Ayar { private array $degerler = ['tema' => 'koyu']; public function __get(string $ad): mixed { return $this->degerler[$ad] ?? null; } } PHP, 'main.php' => <<<'PHP' <?php require 'ayar.php'; var_dump((new Ayar())->tema); PHP, ], [ 'ayar.php' => <<<'PHP' <?php class Ayar { private array $degerler = ['gorunum' => 'koyu']; public function __get(string $ad): mixed { return $this->degerler[$ad] ?? null; } } PHP, ]); foreach (['once', 'sonra'] as $asama) { [$kod, $cikti] = kostur("olcum/sihirli-okuma/$asama"); echo $asama, ' cikis=', $kod, ' cikti=', $cikti, "\n"; }
sihirli-okuma 1 dosya cikis=0 cikti farkli -> ortuk once cikis=0 cikti=string(4) "koyu" sonra cikis=0 cikti=NULL
NM23. Kullanan hiçbir şey görmedi: çıkış 0 kaldı, ama string(4) "koyu" yerine NULL geldi. __get bilinmeyen adı null ile karşılıyordu, yani tema artık yok ve bunu söyleyen bir şey olmadı. Bu dersin birinci ölçü sınıfı örtüktür. Sihirli yöntem, okumayı bir sözleşmeden çıkarıp bir arama işlemine çevirdiği için yeniden adlandırma hiçbir imzayı bozmaz.
Sınırlayıcı Ölçüm
Sessizliğin nedeni yeniden adlandırma mı, yoksa __get’in bilinmeyen adı null ile karşılaması mı? İkinci seçenek ölçülür: aynı yeniden adlandırma yapılır, ama bu kez __get bilinmeyen bir adda OutOfRangeException fırlatır.
<?php // bilinmeyen_ad.php — ayni yeniden adlandirma, bu kez __get bilinmeyen adi reddediyor require_once 'yayilim.php'; olc('bilinmeyen-ad', [ 'ayar.php' => <<<'PHP' <?php class Ayar { private array $degerler = ['tema' => 'koyu']; public function __get(string $ad): mixed { if (!array_key_exists($ad, $this->degerler)) { throw new OutOfRangeException($ad); } return $this->degerler[$ad]; } } PHP, 'main.php' => <<<'PHP' <?php require 'ayar.php'; var_dump((new Ayar())->tema); PHP, ], [ 'ayar.php' => <<<'PHP' <?php class Ayar { private array $degerler = ['gorunum' => 'koyu']; public function __get(string $ad): mixed { if (!array_key_exists($ad, $this->degerler)) { throw new OutOfRangeException($ad); } return $this->degerler[$ad]; } } PHP, ]); foreach (['once', 'sonra'] as $asama) { [$kod, $cikti] = kostur("olcum/bilinmeyen-ad/$asama"); echo $asama, ' cikis=', $kod, ' cikti=', $cikti, "\n"; }
bilinmeyen-ad 1 dosya cikis=255 cikti farkli -> kiran once cikis=0 cikti=string(4) "koyu" sonra cikis=255 cikti=
NM24. Aynı yeniden adlandırma, aynı kullanan; ancak çıkış 255 oldu, çünkü tema adı artık bilinmiyordu ve __get bunu bir istisnayla bildirdi. Sessizliği doğuran yeniden adlandırma değil, __get’in bilinmeyen adı geçiştirmesiydi. Bir sihirli yöntemin yanıtı sınıf yazarının kararıdır: aynı değişiklik null ile örtük, istisnayla kıran olabiliyor.
Özet
- Sihirli yöntemler
__get,__set,__isset,__callve__toStringgibi adları iki alt çizgiyle başlayan ve PHP’nin kendisinin çağırdığı yöntemlerdir. - Okuma, yazma ve yöntem çağrısı sihirli yönteme çevrilince tanımsız ad kullananı durdurmaz.
__get’in baktığı iç anahtartema’dangorunum’a inince kullanan hata almadı, çıkış 0;string(4) "koyu"yerineNULLgeldi, dersin ölçü sınıfı örtüktür.__getbilinmeyen addaOutOfRangeExceptionfırlatınca aynı değişiklik kıran oldu, çıkış 255.- Aynı değişikliğin örtük mü kıran mı olduğunu sihirli yöntemin bilinmeyen adı nasıl karşıladığı belirler.
Sonraki Adım
Sihirli yöntemler gevşek bir çevirici koyuyordu; tip bildirimleri tam tersini yapar ve sınıfın sınırına bir parametre, bir dönüş, bir özellik tipi koyar. Tip Bildirimleri dersi bir yöntemin parametresine int yazmanın kullananı hangi girdilerde durdurduğunu soruyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.