İçeriğe geç
academia.sh

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, __call ve __toString gibi 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ç anahtar tema’dan gorunum’a inince kullanan hata almadı, çıkış 0; string(4) "koyu" yerine NULL geldi, dersin ölçü sınıfı örtüktür.
  • __get bilinmeyen adda OutOfRangeException fı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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat