Ders 09 / 21
Nesne ve Tablo Bağlaması
Tabloyu PHP dizisi olarak tutan küçük bir model sınıfı kurulur; kaydetme kancasının kayıtta koştuğu, izin listesindeki olmayan alanın aşırı atamada bağlanmadığı ve toplu güncellemede kancanın bağlanmadığı ölçülür.
Yayın Güncelleme
Bu derste
- Tabloyu bir model sınıfına bağlayan küçük bir düzenek kurmak
- Kaydetme kancasının kayıt sırasında koştuğunu ölçmek
- İzin listesinde olmayan alanın aşırı atamada bağlanmadığını göstermek
- Toplu güncellemenin kaydetme kancasını çalıştırmadığını gerçek çıktıyla okumak
İçindekiler
Form doğrulaması geçti ve kayıt adımı koştu. Peki kayıt bir model üzerinden yapıldığında, kaydedilirken yalnızca satır mı yazılır? Modelin kendi kancaları, erişimcileri ve alan kuralları da çalışır; hangilerinin çalıştığını görmek bu dersin konusudur.
Nesne–tablo bağlaması (object–relational mapping) bir sınıfı bir tabloya bağlayan düzenektir: sınıfın bir örneği tablonun bir satırıdır. Bu bağlamanın kaynakta ne doğurduğu M08/K06’daki aynı adlı derste ölçülmüştü; bu derste soru farklıdır: model kaydedilirken hangi parça bu istekte koştu. Karar sırası sinifla() fonksiyonunun kendisidir ve bu dersin ağırlıklı sınıfı koştu’dur; ölçümler bağlanmadı ile karşılaştırılır.
GV8. Tablo bir PHP dizisidir, yani veri katmanı bir dizi satırdan ibarettir; gerçek bir veri tabanı bağlantısı yoktur. GV9. Alan adları ASCII’dir ve id kayıt sırasında tablonun boyundan türetilir. GV10. Aşırı atama (mass assignment) karşıtı için okunma adlı nötr bir sayaç alanı kullanılır; alan yetki ya da rol taşımaz. GV11. Slug üretimi yalnızca ASCII harf ve rakamları korur.
Ölçüm düzeneği değişmeden gelir.
<?php // kostu.php — beklenen bir parça bu istekte koştu mu? declare(strict_types=1); final class Gunluk { public static array $yol = []; // isteğin yoluna konan parçalar public static array $kostu = []; // istek içinde çağrılanlar public static array $ertelenen = []; // isteğin dışına bırakılanlar public static function yeniIstek(): void { self::$yol = self::$kostu = self::$ertelenen = []; } public static function yola(string $parca): void { self::$yol[] = $parca; } public static function kostu(string $parca): void { self::$kostu[] = $parca; } public static function ertele(string $parca): void { self::$ertelenen[] = $parca; } } function sinifla(string $parca): string { return match (true) { in_array($parca, Gunluk::$kostu, true) => 'koştu', in_array($parca, Gunluk::$ertelenen, true) => 'ertelendi', in_array($parca, Gunluk::$yol, true) => 'atlandı', default => 'bağlanmadı', }; }
Modeli Kurmak
Model tabanı üç şey bilir: hangi tabloya yazdığını, hangi alanların toplu doldurulabildiğini ve kaydetme sırasında hangi kancayı çağıracağını. Yaşam döngüsü kancası (lifecycle hook) bir olayın öncesinde ya da sonrasında çağrılan yöntemdir; burada kaydediliyor kaydın tabloya yazılmasından hemen önce çağrılır. Erişimci (accessor) bir alanı okurken değeri dönüştüren yöntemdir; burada okuOzetKisa, ozetKisa adıyla okunur. Sorgu kapsamı (query scope) sık kullanılan bir süzgeci adlandırır; yayinda() yalnızca yayınlanmış satırları döndürür.
doldur() yalnızca izin listesindeki alanları alır. İzin listesinde olmayan bir alan için ne bir hata ne bir günlük kaydı vardır: alan, parçanın yoluna hiç konmaz. Bu dersin karşıt ölçümü bu sessizliğe dayanır.
<?php // model.php — tablo dizisi, kapsam, erişimci ve kancalı küçük bir model declare(strict_types=1); require_once 'kostu.php'; final class Veri { public static array $tablolar = ['yazilar' => []]; public static function sifirla(): void { self::$tablolar = ['yazilar' => []]; } } abstract class Model { protected static string $tablo; protected static array $doldurulabilir = []; public array $ozellikler = []; public static function olustur(array $girdi): static { return (new static())->doldur($girdi)->kaydet(); } public function doldur(array $girdi): static { foreach ($girdi as $alan => $deger) { if (in_array($alan, static::$doldurulabilir, true)) { Gunluk::yola("alan:$alan"); Gunluk::kostu("alan:$alan"); $this->ozellikler[$alan] = $deger; } } return $this; } public function kaydet(): static { Gunluk::yola('kanca:kaydediliyor'); $this->kaydediliyor(); $this->ozellikler['id'] = count(Veri::$tablolar[static::$tablo]) + 1; Veri::$tablolar[static::$tablo][] = $this->ozellikler; return $this; } protected function kaydediliyor(): void {} public static function toplu(array $degerler, array $kosul): int { $say = 0; foreach (Veri::$tablolar[static::$tablo] as &$satir) { if (array_intersect_assoc($kosul, $satir) === $kosul) { $satir = $degerler + $satir; $say++; } } return $say; } public function __get(string $ad): mixed { $erisimci = 'oku' . ucfirst($ad); return method_exists($this, $erisimci) ? $this->$erisimci() : ($this->ozellikler[$ad] ?? null); } } final class Yazi extends Model { protected static string $tablo = 'yazilar'; protected static array $doldurulabilir = ['baslik', 'ozet', 'yayinda']; protected function kaydediliyor(): void { Gunluk::kostu('kanca:kaydediliyor'); $baslik = strtolower((string) $this->ozellikler['baslik']); $this->ozellikler['slug'] = trim(preg_replace('/[^a-z0-9]+/', '-', $baslik), '-'); } protected function okuOzetKisa(): string { return mb_substr((string) $this->ozellikler['ozet'], 0, 5) . '...'; } public static function yayinda(): array { return array_values(array_filter(Veri::$tablolar['yazilar'], fn($s) => $s['yayinda'] ?? false)); } } function raporla(array $parcalar): void { foreach ($parcalar as $parca) printf(" %-20s %s\n", $parca, sinifla($parca)); }
Kaydetme Kancasının Koştuğunu Görmek
Ölçüm iki yazı oluşturur: biri yayında, öteki değil. Beklenen: kaydetme kancası koştu, başlıktan ilk-yazi slug’ı üretildi, kapsam yalnızca yayındaki satırı döndürür, erişimci özeti kısaltır. İzin listesindeki alanların üçü de koştu.
<?php // gozlem.php — kaydetme kancası, kapsam ve erişimci require_once 'model.php'; Veri::sifirla(); Gunluk::yeniIstek(); $yazi = Yazi::olustur(['baslik' => 'Ilk Yazi', 'ozet' => 'Kisa ozet metni', 'yayinda' => true]); Yazi::olustur(['baslik' => 'Taslak', 'ozet' => 'Henuz hazir degil', 'yayinda' => false]); echo "slug={$yazi->slug}\n"; echo "ozetKisa={$yazi->ozetKisa}\n"; echo "yayinda=" . count(Yazi::yayinda()) . " / toplam=" . count(Veri::$tablolar['yazilar']) . "\n"; raporla(['alan:baslik', 'alan:ozet', 'alan:yayinda', 'kanca:kaydediliyor']);
slug=ilk-yazi ozetKisa=Kisa ... yayinda=1 / toplam=2 alan:baslik koştu alan:ozet koştu alan:yayinda koştu kanca:kaydediliyor koştu
Kanca, kaydı yazan çağrının içinde kendiliğinden koştu; çağıranın bir şey yapması gerekmedi, slug kendiliğinden üretildi. Bu, kancanın yararı ve aynı zamanda kırılganlığıdır: davranış çağıranın göremediği bir yerde durur. Kapsam ve erişimci ayrı birer koşu kaydı bırakmadı, çünkü bunlar çağrılarak çalışan yöntemlerdir ve ölçülen şey bir adımın isteğe katılıp katılmadığıdır.
Karşıt Ölçüm: İzin Listesinde Olmayan Alan
Aşırı atama, bir isteğin gövdesindeki alanların topluca modele aktarılmasıdır; izin listesi bu aktarımın sınırıdır. Ölçüm isteğe izin listesinde olmayan okunma alanını ekler. Beklenen: okunma alanı bağlanmadı, çünkü hiç yola konmadı; değer kayda girmez; hata yoktur.
<?php // asiri.php — izin listesinde olmayan alan aşırı atamada bağlanmaz require_once 'model.php'; Veri::sifirla(); Gunluk::yeniIstek(); $yazi = Yazi::olustur(['baslik' => 'Ikinci', 'ozet' => 'Metin', 'yayinda' => true, 'okunma' => 999]); echo "okunma kayitta " . (array_key_exists('okunma', Veri::$tablolar['yazilar'][0]) ? 'var' : 'yok') . "\n"; raporla(['alan:baslik', 'alan:okunma']);
okunma kayitta yok alan:baslik koştu alan:okunma bağlanmadı
okunma istekte vardı, kayıtta yok. Sınıf atlandı değil bağlanmadı: bir parça yola konup çağrılmazsa atlandı olurdu; burada izin listesi alanı yola koyma adımının dışında bıraktı. Hata verilmemesi, aşırı atamada sessizliğin hem koruyucu hem de aldatıcı olabileceği anlamına gelir. Alan neden girmedi sorusunun cevabı çıktıda değil izin listesindedir.
Sınırlayıcı Ölçüm
Kanca kayıt yolunda çalışır. Peki kayıt yolunu atlayan bir güncelleme yolu varsa? toplu() doğrudan tablo satırlarını değiştirir, kaydet() çağrılmaz. Ölçüm önce bir yazı oluşturur, sonra başlığını toplu() ile değiştirir. Beklenen: kanca bağlanmadı ve slug eski başlıkta kalır.
<?php // toplu.php — toplu güncelleme kaydetme kancasını çalıştırmaz require_once 'model.php'; Veri::sifirla(); Yazi::olustur(['baslik' => 'Ilk Yazi', 'ozet' => 'Metin', 'yayinda' => true]); Gunluk::yeniIstek(); $say = Yazi::toplu(['baslik' => 'Yeni Baslik'], ['id' => 1]); $satir = Veri::$tablolar['yazilar'][0]; echo "guncellenen=$say\n"; echo "baslik={$satir['baslik']} slug={$satir['slug']}\n"; raporla(['kanca:kaydediliyor']);
guncellenen=1 baslik=Yeni Baslik slug=ilk-yazi kanca:kaydediliyor bağlanmadı
Çıktıda kanca bağlanmadı: başlık değişti, slug değişmedi. Slug başlığa bağlı bir türetilmiş değerdir ve kanca atlandığında tutarsızlaşır. Bağlanmayan kanca hata vermez, bu yüzden yanıtta da görünmez. Bu sınıf tehlikelidir: davranışın beklendiği yerde hiçbir iz yoktur. Çözüm, değişikliğin kaydet yolundan geçirilmesidir ya da toplu güncellemenin türetilmiş alanı kendisinin yazmasıdır; hangisinin seçildiği çağıranın kararıdır.
Özet
- Model, bir sınıfı bir tabloya bağlar; kapsam, erişimci ve kanca bu bağlamanın üstüne eklenir.
- Bu dersin ölçü sınıfı
koştu’dur: kaydetme kancası kayıt yolunda kendiliğinden koştu ve başlıktan slug üretti. - İzin listesinde olmayan alan aşırı atamada
bağlanmadı: değer kayda girmedi ve hata da vermedi. - Kayıt yolunu atlayan toplu güncellemede kanca
bağlanmadıve türetilmiş alan eski kaldı. - Sessizlik koruma da olabilir aldatıcı da; neden kayda girmediği çıktıda değil izin listesindedir.
Sonraki Adım
Model tek tabloyu ele aldı. Bir yazının yazarı başka tabloda durduğunda, o ilişki sorgusu ne zaman çalışır? Bir sonraki ders ilişkileri ve tembel yüklemeyi ölçer; ilişki sorgusunun ertelendiği sınıfı ele alır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.