PHP 8 öznitelikleri, çerçevenin yapılandırmasını kodun yanına taşımayı mümkün kılıyor. Bir modelin hangi alanların doldurulabilir olduğu, hangilerinin gizli kalacağı ya da hangi alanın rota anahtarı olacağı artık sınıfın gövdesinde dağınık dizilerle değil, sınıfın üstünde tek satırlık bildirimlerle yazılabiliyor.

Bu metinde, bu geçişin ne kazandırdığını ve nerede dikkat etmek gerektiğini anlatacağım. Örnekler, bu sitenin kendi modelleri üzerinden: yazı, kullanıcı ve yorum modelleri tamamen öznitelik tabanlı yazıldı. Böylece bir modelin tanımını açtığınızda, neyin nasıl davrandığını ilk satırlardan okuyabiliyorsunuz.

Eski yöntem: gövde içinde dağınık diziler

Laravel uzun süre modellerin özelliklerini sınıf gövdesindeki özellikler ve metotlarla tanımladı. Doldurulabilir alanlar bir dizi, gizli alanlar başka bir dizi, rota anahtarı ise ayrı bir metot olarak yazılırdı. Bir model büyüdükçe bu tanımlar birbirinden uzaklaşır, bazen de unutulur.

class Post extends Model
{
    protected $fillable = ['title', 'slug', 'body'];

    protected $hidden = ['search_text'];

    public function getRouteKeyName(): string
    {
        return 'slug';
    }
}

Bu kodda bir sorun yok. Ama her model biraz farklı bir düzen kullandığında, bir dosyadan diğerine geçmek zorlaşır. Özniteliklerin ne zaman tercih edileceği de bu nedenle bir zevk meselesi olmaktan çıkar ve ekibin ortak bir standarda ihtiyacı doğar.

Yeni yöntem: sınıfın üstünde bildirim

Öznitelikler aynı bilgiyi, sınıfın tanımının hemen üstünde, okunması kolay bir biçimde taşır. Bu sitedeki Post modelinin başlığı şöyle:

#[Fillable(['category_id', 'user_id', 'title', 'slug', 'excerpt', 'body'])]
#[RouteKey('slug')]
class Post extends Model
{
    //
}

Bu iki satır, gövdedeki üç metodun yerini alır. Modelin dışarıdan nasıl kullanılacağı, sınıf açılır açılmaz görülebilir. Ayrıca bu öznitelikler, yardımcı araçların ve editörlerin modeli okumasını kolaylaştırır.

Yapılandırmanın koda yakınlığı, okunabilirliğin en ucuz biçimidir.

Gizli alanlar ve doldurulabilir alanlar

Gizli alanlar, bir modelin JSON ya da dizi çıktısına girmeyen alanlarıdır. Şifre ya da hatırlatma belirteci gibi değerler bu gruba aittir. Bu bilgi de sınıfın üstünde, tek satırda yazılabilir:

#[Fillable(['name', 'email', 'password'])]
#[Hidden(['password', 'remember_token'])]
class User extends Authenticatable
{
    //
}

Bu bildirimin sağladığı en büyük fayda, gizli alanların tek bir bakışta görünmesidir. Bir alanın JSON çıktısına sızıp sızmadığını anlamak için gövdeyi taramaya gerek kalmaz.

ÖznitelikNe belirlerÖrnek
FillableToplu atamada yazılabilen alanlar#[Fillable(['title'])]
HiddenDizi ve JSON çıktısından gizlenen alanlar#[Hidden(['password'])]
RouteKeyRota modeli bağlamasında kullanılan alan#[RouteKey('slug')]
ScopeSorgu kapsamını işaretleyen metot#[Scope] protected function published(...)

Rota anahtarı: adres çubuğundaki güvenli değer

Bir yazının adresinde kimliği (/yazilar/412) yazmak yerine, okunabilir bir ad (/yazilar/htmx-ile-spa-hissi) kullanmak hem kullanıcıya hem de arama motorlarına daha anlamlı bir adres sunar. RouteKey özniteliği, bu adın rota modeli bağlamasında kullanılacağını belirtir.

Burada bir dikkat noktası var: rota anahtarı değiştiğinde, eski adresler kırılır. Bu nedenle slug'ı bir kez yayınladıktan sonra değiştirmek yerine, eski adresleri yeni adrese yönlendiren bir kayıt tutmak gerekir. Bu sitede yönlendirmeler için ayrı bir tablo ve akış bulunuyor.

Route::get('/yazilar/{post}', [BlogController::class, 'show'])
    ->name('blog.show');

// Post::resolveRouteBinding() çağrısı, #[RouteKey('slug')] sayesinde slug ile çözer.

Kapsamlar: sorguyu adlandırmak

Sık kullanılan sorgu koşulları, kapsam (scope) olarak modele taşınır. Yayında olan yazılar, bir kategoriye ait yazılar ya da son yazılar gibi koşullar her yerde aynı biçimde yazılmak zorunda kalmaz. Öznitelik #[Scope] ile işaretlenen metot, sorgu oluşturucuda doğrudan çağrılabilir hâle gelir.

#[Scope]
protected function published(Builder $query): void
{
    $query->whereIn('status', PostStatus::publiclyVisible())
        ->whereNotNull('published_at')
        ->where('published_at', '<=', now());
}

// Kullanım
Post::query()->published()->latestFirst()->paginate(8);

Bu yaklaşımın gücü, kuralın tek bir yerde durmasıdır. "Yayında" kavramının ne anlama geldiği değiştiğinde, yalnızca bu metot güncellenir. Kapsam adı, sorgunun niyetini de taşır: published() ifadesi, neyi istediğinizi açıkça söyler.

Casts ve gövdede kalanlar

Öznitelikler her şeyi kapsamaz. Alan tiplerinin dönüşümü hâlâ casts() metodunda yazılır. Bu bilinçli bir tercih: dönüşümler ilişkiler ve mantıkla birlikte çalışır, bu yüzden bir metot olarak kalmaları daha doğaldır.

protected function casts(): array
{
    return [
        'status' => PostStatus::class,
        'is_featured' => 'boolean',
        'published_at' => 'datetime',
    ];
}

İlişkiler de gövdede kalır. Bir yazının yazarını, kategorisini ya da yorumlarını tanımlayan metotlar, sorgu oluşturucuyla birlikte çalıştığı için bir öznitelik değil, bir metottur.

Geçişi nasıl yaparsınız?

Mevcut bir modeli öznitelik tabanlı yazmak, acele edilmeyecek bir iş değil; ama büyük bir yeniden yazım da gerekmez. Modeller tek tek, her seferinde küçük bir adımla taşınabilir.

  1. Modelin $fillable dizisini sınıfın üstüne #[Fillable] olarak taşıyın.
  2. $hidden dizisini #[Hidden] ile değiştirin.
  3. getRouteKeyName() metodunu #[RouteKey] ile değiştirin.
  4. Kapsam metotlarına #[Scope] ekleyin; sorgu çağrılarını değiştirmenize gerek yok.
  5. Her adımdan sonra ilgili testleri çalıştırın.

Geçişin en önemli kontrolü, doldurulabilir alanların değişmediğinden emin olmaktır. Bir alan eksik kalırsa, toplu atama sessizce başarısız olabilir. Bu yüzden her modelin test kapsamında, izin verilen alanların listesi doğrulanmalıdır.

Dikkat edilmesi gerekenler

Öznitelikler okunabilirliği artırır, ama bazı durumlarda yanıltıcı olabilir. İlk olarak, bir özniteliğin sınıfın üstünde olması, onun sınıfın davranışını değiştirdiği anlamına gelmez; yalnızca bir yapılandırma bildirir. Bu ayrımı ekibin herkesinin bilmesi gerekir.

İkinci olarak, öznitelikleri kullanan kod, Laravel sürümüne bağlıdır. Projenin kullandığı sürümün bu öznitelikleri desteklediğini ve dokümantasyonu doğrulandığını kontrol edin. Sürüm yükseltmelerinde bu bildirimlerin nasıl ele alındığını da gözden geçirmek iyi bir alışkanlıktır.

  • Bir modelde iki farklı stil (öznitelik ve gövde) karışık kalmamalı; tutarlılık ekibin ortak kuralıdır.
  • Doldurulabilir alan listesi uzadığında, modelin sorumluluğunu yeniden düşünmek gerekebilir.
  • Gizli alanları, API yanıtlarını da kapsayacak şekilde test edin.

Eski ve yeni yaklaşımı yan yana okumak

Bir geçişin değerini görmek için, iki yaklaşımı aynı model üzerinde karşılaştırmak en açık yoldur. Aşağıdaki tablo, bu sitenin User modelindeki bir tanımın eski ve yeni biçimini yan yana verir.

KonuGövde içindeÖznitelikle
Doldurulabilir alanlarprotected $fillable = [...]#[Fillable([...])]
Gizli alanlarprotected $hidden = [...]#[Hidden([...])]
Rota anahtarıgetRouteKeyName() metodu#[RouteKey('slug')]
Tip dönüşümlericasts() metoduHâlâ casts() metodu
İlişkilerMetotHâlâ metot

Tablo, geçişin sınırını da gösterir. Öznitelikler yapılandırmayı taşır; ilişkiler ve dönüşümler ise davranış taşır. Bu ayrım, hangi kodun nerede yazılacağına karar vermeyi kolaylaştırır.

Modelin davranışını testle güvence altına almak

Bir modeli öznitelik tabanlı yazmak, davranışın değişmediğinden emin olmayı gerektirir. Özellikle gizli alanların API yanıtlarına sızmaması kritiktir. Bu kontrolü küçük bir testle yapabilirsiniz:

public function test_user_array_hides_password_and_remember_token(): void
{
    $user = User::factory()->create();

    $array = $user->toArray();

    $this->assertArrayNotHasKey('password', $array);
    $this->assertArrayNotHasKey('remember_token', $array);
}

Doldurulabilir alanlar için de benzer bir test yazılabilir. Toplu atamada beklenen alanların kabul edildiğini, beklenmeyenlerin ise yok sayıldığını doğrulamak, yanlışlıkla bir alanı açmanın önüne geçer. Bu testler küçük olsa da, geçiş sırasında en çok değer sağlayan güvencedir.

Kapsamları sorgularda yeniden kullanmak

Kapsamların asıl gücü, sorguları birbirine zincirlemekte ortaya çıkar. Bu sitede yazı listeleri, yayında olma, kategori ve sıralama koşullarını bir araya getirir. Her koşul ayrı bir metotta yaşadığı için, aynı kombinasyonu farklı yerlerde yeniden yazmanız gerekmez.

$gezi = Post::query()
    ->published()
    ->inCategory('gezi')
    ->latestFirst()
    ->paginate(8);

Bu sorgu, okunduğunda ne istediğini söyler: yayında olan, gezi kategorisindeki, en yeniden eskiye sıralanmış yazılar. Koşulların nasıl uygulandığını bilmek gerekmez; yalnızca ne anlama geldiklerini bilmek yeterlidir. Bu da okuyucunun bilişsel yükünü azaltır.

Kapsamın bir sınırı da vardır: kapsam her zaman sorguyu daraltır, ama genişletmez. Farklı bir koşul kümesine ihtiyaç varsa, yeni bir kapsam yazmak ya da bir sorgu metodu eklemek daha doğru olur.

Öznitelik kullanılmaması gereken durumlar

Her model öznitelik ile yazılmalı mıdır? Hayır. Bazı durumlarda gövde içindeki yazım daha net olur. Bir modelin doldurulabilir alan listesi karmaşık koşullara bağlıysa, örneğin kullanıcı rolüne göre değişiyorsa, bu koşullar bir metotta yazılmalıdır. Öznitelik yalnızca sabit ve tek anlamlı bir bildirim için uygundur.

Ayrıca $guarded ile #[Fillable] birlikte kullanılmamalıdır. İki yaklaşım aynı işi farklı yollarla yaptığında, hangisinin geçerli olduğu okuyucu için belirsiz kalır. Ekip bir yaklaşımı seçtiğinde, diğerini kaldırmak da bu geçişin parçası olmalıdır.

Bir kuralın iki farklı yerde yazılı olması, o kuralın birinin yanlış olduğu anlamına gelir.

Ekip için bir stil rehberi

Geçişin uzun vadede değer üretmesi için, ekibin tutarlı bir düzen benimsemesi gerekir. Bir rehber, hangi bildirimin hangi sırayla yazılacağını, boş bir sınıfın nasıl görüneceğini ve yeni bir model oluştururken hangi dosyaların eklenmesi gerektiğini söyler.

  • Sınıf üstündeki öznitelikler: önce Fillable, sonra Hidden, en son RouteKey.
  • Gövde: önce casts(), sonra kapsamlar, en sonda ilişkiler.
  • Her yeni model için bir fabrika ve bir test; alan listesini doğrulayan kısa bir test eklenir.

Bu rehber bir kez yazılır, ama her yeni model onu kullanır. Yeni katılan biri, hangi modelin nasıl yazılacağını tahmin etmek zorunda kalmaz.

Sonuç

Laravel'in öznitelik tabanlı model tanımı, bir modelin ne olduğunu sınıfın ilk satırlarında söyleyebilme imkânı verir. Doldurulabilir alanlar, gizli alanlar, rota anahtarı ve kapsamlar, tek bir bakışta okunur hâle gelir. Bu sadelik, büyük projelerde bakım maliyetini belirgin biçimde düşürür.

Bu geçişi başlatmak için kısa bir liste:

  1. Projenin Laravel sürümünün öznitelikleri desteklediğini doğrulayın.
  2. En küçük modelle başlayın ve testlerin geçtiğini görün.
  3. Ekip içinde bir stil kuralı belirleyin ve yeni modellerde uygulayın.
  4. Eski modelleri, bir değişiklik yaparken birlikte taşıyın.
İyi bir model, dosyasını açan kişiye neyi bildiğini söyler; öznitelikler bu konuşmanın ilk cümleleridir.