PHP attributes (öznitelikler), sınıflara, metotlara, fonksiyonlara, parametrelere, özelliklere ve sabitlere #[...] sözdizimiyle eklenen, makinenin okuyabileceği yapılandırılmış üst verilerdir. Örneğin #[Rota('/urunler')] yazdığınızda kodun davranışı kendiliğinden değişmez; bu bilgi Reflection API ile çalışma anında okunur ve bir yönlendirici, doğrulayıcı veya komut kaydedici gibi bir araç ona göre hareket eder. PHP'nin kendisi de #[\Override], #[\Deprecated] ve #[\SensitiveParameter] gibi motorun doğrudan anladığı hazır öznitelikler sunar.

Öznitelikler PHP 8.0 ile geldi ve sonraki sürümlerde yeni hazır öznitelikler eklendi. Bu yazıda sözdizimini, kendi öznitelik sınıfınızı yazmayı, hedef ve tekrar kısıtlarını, Reflection ile okumayı, hataların neden newInstance() anında ortaya çıktığını ve hazır özniteliklerin ne işe yaradığını çalışan örneklerle anlatıyoruz. Örnekler PHP 8.5.12 ile çalıştırıldı. Serinin diğer konuları için PHP öğrenme rehberi sayfasına bakabilirsiniz.

Öznitelik Nedir, Arayüzden Farkı Ne?

Öznitelik Nedir, Arayüzden Farkı Ne?

Kılavuz öznitelikleri, bir özelliğin uygulamasını onun kullanıldığı yerden ayırmanın bildirimsel (declarative) bir yolu olarak tanımlar. Arayüzler bir yapı dayatır: arayüzü uygulayan sınıf belirli metotları yazmak zorundadır. Öznitelikler ise yapıyı değiştirmeden koda not düşer. Kılavuzdaki örnekte bir işlem arayüzü vardır ve bazı işlemler çalışmadan önce hazırlık adımı ister. Her sınıfa boş bir setUp() metodu zorlamak yerine hazırlık yapan metotlar #[SetUp] ile işaretlenir ve çalıştırıcı bu işaretli metotları bulup sırayla çağırır. Bir öznitelik birden fazla yerde, hatta gerekirse aynı yerde birden çok kez kullanılabilir.

Bu ayrımı akılda tutmak işe yarar: davranış sözleşmesi gerekiyorsa arayüz, "bu metot şu adrese bağlı", "bu alan zorunlu", "bu parametre gizli" gibi bir bilgi gerekiyorsa öznitelik. Arayüzler için kalıtım, abstract ve interface yazısına bakabilirsiniz.

Sözdizimi: #[...] Nasıl Yazılır?

Bir öznitelik bildirimi #[ ile başlar ve ] ile biter. İçine virgülle ayrılmış bir veya daha fazla öznitelik yazılabilir. Öznitelik adı bir sınıf adı gibi çözümlenir; yani niteliksiz, nitelikli veya tam nitelikli (\ ile başlayan) olabilir ve use ile içe aktarılabilir. Argümanlar isteğe bağlıdır, parantez içinde yazılır ve yalnızca sabit değerler veya sabit ifadeler olabilir; konumsal ve isimli argümanların ikisi de desteklenir. PHP 8.5 sürüm notlarına göre closure'lar ve first-class callable'lar da artık sabit ifadelerde, dolayısıyla öznitelik argümanlarında kullanılabiliyor.

php
<?php
namespace Uygulama;

use Attribute;

#[Attribute]
class Etiket
{
    public const VARSAYILAN = 'genel';

    public function __construct(public string $ad = self::VARSAYILAN, public int $oncelik = 0) {}
}

#[Etiket]                                   // argümansız
#[\Uygulama\Etiket('rapor')]                // tam nitelikli ad
#[Etiket(ad: 'yonetim', oncelik: 2)]        // isimli argümanlar
#[Etiket(Etiket::VARSAYILAN, 1 + 1)]        // sabit ifade
class RaporServisi {}

#[Etiket('a'), Etiket('b')]                 // tek bildirimde iki öznitelik
class DigerServis {}

echo count((new \ReflectionClass(RaporServisi::class))->getAttributes()), "\n"; // çıktı: 4

Bu örnekte Etiket aynı sınıfa dört kez uygulandı ve hata alınmadı. Bunun nedeni, örnek yalnızca öznitelikleri saydı, hiçbirinden nesne üretmedi. Normalde bir öznitelik aynı bildirimde tek kez kullanılabilir; tekrar kuralı, aşağıda göreceğimiz gibi ancak newInstance() çağrıldığında denetlenir. Öznitelik adının use kurallarına göre çözümlenmesi nedeniyle, ad alanı içindeki dosyalarda hazır öznitelikleri #[\Override] gibi baştaki ters bölüyle yazmak veya use Attribute; eklemek gerekir. Ad çözümleme kuralları namespace ve use yazısında anlatılıyor.

Kendi Öznitelik Sınıfınızı Tanımlamak

Kendi Öznitelik Sınıfınızı Tanımlamak: Attribute::TARGET_CLASS, Attribute::TARGET_FUNCTION, Attribute::TARGET_METHOD, Attribute::TARGET_PROPERTY

Kılavuz her öznitelik için ayrı bir sınıf tanımlamayı önerir. En basit hali, #[Attribute] ile işaretlenmiş boş bir sınıftır. Öznitelik bir nesneye dönüştürüldüğünde, bildirimdeki argümanlar bu sınıfın kurucusuna aktarılır; bu yüzden kurucu imzası, özniteliğin hangi argümanları kabul ettiğini belirler. Kurucu özellik tanıtımı burada çok kullanışlıdır.

#[Attribute] bildirimine ilk argüman olarak bir bit maskesi verilerek özniteliğin nereye uygulanabileceği kısıtlanır. Varsayılan değer Attribute::TARGET_ALL'dur. Aynı yerde birden fazla kullanıma izin vermek için maskeye Attribute::IS_REPEATABLE eklenir.

SabitUygulanabildiği yerNot
Attribute::TARGET_CLASSSınıf (enum dahil)
Attribute::TARGET_FUNCTIONFonksiyon
Attribute::TARGET_METHODMetot
Attribute::TARGET_PROPERTYÖzellik
Attribute::TARGET_CLASS_CONSTANTSınıf sabiti (enum case dahil)
Attribute::TARGET_PARAMETERFonksiyon veya metot parametresi
Attribute::TARGET_CONSTANTSınıf dışındaki const sabitiPHP 8.5 ile eklendi
Attribute::TARGET_ALLHepsiVarsayılan
Attribute::IS_REPEATABLEHedef değil, bayrakAynı yerde tekrar kullanıma izin verir

Enum'lar için kılavuz, TARGET_CLASS hedefinin enum'un kendisini, TARGET_CLASS_CONSTANT hedefinin ise enum case'lerini kapsadığını belirtir; enum ayrıntıları enum yazısında. PHP 8.5 ile bir değişiklik daha geldi: #[\Attribute] soyut bir sınıfa, enum'a, arayüze veya trait'e uygulanırsa hata artık derleme anında verilir. PHP 8.5.12'de #[Attribute] abstract class Soyut {} yazmak Fatal error: Cannot apply #[\Attribute] to abstract class Soyut ile sonuçlanır. Önceden bu hata ancak newInstance() çağrıldığında görülüyordu.

Reflection ile Öznitelik Okumak

Öznitelikleri okumak için ilgili Reflection nesnesinin getAttributes() metodu kullanılır: sınıf için ReflectionClass, metot için ReflectionMethod, fonksiyon için ReflectionFunction, özellik için ReflectionProperty, parametre için ReflectionParameter, sınıf sabiti için ReflectionClassConstant. Bu metot bir ReflectionAttribute dizisi döndürür. Her elemandan getName() ile adı, getArguments() ile argümanları, getTarget() ile hedef bit maskesini, isRepeated() ile tekrar edilip edilmediğini öğrenir ve newInstance() ile öznitelik sınıfının bir örneğini oluşturursunuz.

php
<?php
#[Attribute(Attribute::TARGET_CLASS)]
class Tablo
{
    public function __construct(public string $ad, public string $sema = 'public') {}
}

#[Tablo('urunler', sema: 'magaza')]
class Urun {}

$attr = (new ReflectionClass(Urun::class))->getAttributes()[0];

var_dump($attr->getName());        // çıktı: string(5) "Tablo"
var_dump($attr->getArguments());
/* çıktı:
array(2) {
  [0]=>
  string(7) "urunler"
  ["sema"]=>
  string(6) "magaza"
}
*/
var_dump($attr->getTarget() === Attribute::TARGET_CLASS); // çıktı: bool(true)

$tablo = $attr->newInstance();
echo $tablo->sema . '.' . $tablo->ad, "\n";              // çıktı: magaza.urunler

getArguments() çıktısına dikkat edin: konumsal argüman 0 anahtarıyla, isimli argüman ise kendi adıyla gelir. Bu dizi henüz hiçbir doğrulamadan geçmemiştir. Argümanların tipi, eksik zorunlu argüman ya da sınıfın var olup olmadığı yalnızca newInstance() çağrıldığında denetlenir. Kılavuz bu ayrımı bilinçli bir tasarım olarak açıklar: öznitelik temsili ile gerçek örnek ayrı tutulduğunda hata yönetimini siz kontrol edersiniz.

getAttributes() metoduna bir sınıf adı verirseniz yalnızca o sınıfın öznitelikleri döner. İkinci argüman olarak ReflectionAttribute::IS_INSTANCEOF bayrağını verdiğinizde ise eşleşme instanceof ile yapılır: verilen sınıfın alt sınıfları ve verilen arayüzü uygulayan öznitelikler de gelir. Bu, birden fazla kural sınıfını ortak bir arayüz altında toplamanın anahtarıdır; aşağıdaki doğrulama örneği tam olarak bunu kullanıyor.

Hatalar Neden newInstance() Anında Çıkar?

Kullanıcı tanımlı özniteliklerde hedef ve tekrar kuralları derleme anında değil, nesne üretilirken denetlenir. Var olmayan bir öznitelik sınıfı bile yazabilirsiniz; getName() sorunsuz çalışır, hata ancak nesne istendiğinde gelir. Bu, öznitelik kullanan bir kütüphanenin yüklü olmadığı ortamlarda kodun yine de çalışabilmesini sağlar, ama hataları geç fark etmenize de yol açabilir.

php
<?php
#[Attribute(Attribute::TARGET_METHOD)]
class Rota
{
    public function __construct(public string $yol) {}
}

#[Rota('/yanlis')]
class SinifaKonmus {}

class Denetleyici
{
    #[Rota('/a'), Rota('/b')]
    public function liste() {}
}

#[Yok(1)]
function islem() {}

$denemeler = [
    (new ReflectionClass(SinifaKonmus::class))->getAttributes()[0],
    (new ReflectionMethod(Denetleyici::class, 'liste'))->getAttributes()[0],
    (new ReflectionFunction('islem'))->getAttributes()[0],
];

foreach ($denemeler as $attr) {
    try {
        $attr->newInstance();
    } catch (Error $e) {
        echo $attr->getName(), ': ', $e->getMessage(), "\n";
    }
}
/* çıktı:
Rota: Attribute "Rota" cannot target class (allowed targets: method)
Rota: Attribute "Rota" must not be repeated
Yok: Attribute class "Yok" not found
*/

Üç farklı hata da Error türünde fırlatıldı. Öznitelikleri okuyan bir araç yazıyorsanız newInstance() çağrısını try/catch içine almak ve hatayı hangi sınıf ya da metotta bulduğunuzu da mesaja eklemek, sorunu bulmayı çok kolaylaştırır. Bu davranışın hazır özniteliklerden farklı olduğunu unutmayın: motorun kendi öznitelikleri, örneğin #[\Override], derleme anında denetlenir.

PHP'nin Hazır Öznitelikleri

PHP'nin Hazır Öznitelikleri: #[\Override]: Yanlış Yazılmış Metot Adını Yakalamak, #[\Deprecated]: Kendi Kodunuzu Kullanımdan Kaldırmak, #[\SensitiveParameter]: Şifreyi Yığın...

PHP'nin kendi öznitelikleri motor tarafından doğrudan yorumlanır; onları okumak için Reflection kodu yazmanız gerekmez. Aşağıdaki tabloda kılavuzdaki sürüm bilgileriyle birlikte en sık kullanılanlar var.

ÖznitelikNe yapar?Sürüm
#[\Attribute]Bir sınıfı öznitelik sınıfı olarak işaretler8.0
#[\ReturnTypeWillChange]İç sınıf metodunu ezen metottaki dönüş tipi uyumsuzluğu uyarısını susturur8.1
#[\AllowDynamicProperties]Sınıfta dinamik özellik oluşturmaya izin verir8.2
#[\SensitiveParameter]Parametre değerini yığın izlerinde gizler8.2
#[\Override]Metodun üst sınıftaki veya arayüzdeki bir metodu ezdiğini garanti eder8.3, özelliklerde 8.5
#[\Deprecated]Kullanıldığında E_USER_DEPRECATED uyarısı üretir8.4, trait ve sabitlerde 8.5
#[\NoDiscard]Dönüş değeri kullanılmazsa uyarı verir8.5
#[\DelayedTargetValidation]Hazır özniteliklerin hedef hatasını derlemeden çalışma anına erteler8.5

Tablodaki son satır ilk bakışta garip gelebilir. Hazır öznitelikler yanlış bir hedefe uygulandığında, kullanıcı tanımlı özniteliklerin aksine, hata derleme anında verilir. #[\DelayedTargetValidation] aynı bildirime eklendiğinde bu denetim, öznitelik Reflection ile örneklenene kadar ertelenir. Kılavuz bunun ileriye dönük uyumluluk için düşünüldüğünü söyler: gelecek bir PHP sürümünde yeni hedefler kazanacak bir hazır özniteliği bugünden kullanan kod, eski sürümlerde derleme hatasıyla kırılmaz. Günlük uygulama kodunda nadiren ihtiyaç duyulur, daha çok farklı PHP sürümlerini destekleyen kütüphaneler içindir.

#[\Override]: Yanlış Yazılmış Metot Adını Yakalamak

#[\Override], işaretlediği metodun üst sınıftaki veya uygulanan arayüzdeki bir metodu ezmesi gerektiğini söyler. Eşleşen metot yoksa derleme anında hata verilir. Bu sayede üst sınıftaki metot adı değiştiğinde veya alt sınıfta ad yanlış yazıldığında, metodun sessizce yeni ve hiç çağrılmayan bir metoda dönüşmesini önlersiniz. PHP 8.5 ile özelliklere de uygulanabiliyor. Kurucu metot (__construct) imza denetiminden muaf olduğu için bu öznitelik orada kullanılamaz.

php
<?php
class Depo
{
    protected function kaydet(): void {}
}

final class DosyaDeposu extends Depo
{
    #[\Override]
    protected function kaydt(): void {}   // harf hatası
}
// Fatal error: DosyaDeposu::kaydt() has #[\Override] attribute, but no matching parent method exists in /yol/ornek.php on line 10

#[\Deprecated]: Kendi Kodunuzu Kullanımdan Kaldırmak

PHP 8.4 ile gelen bu öznitelik fonksiyonları, metotları ve sınıf sabitlerini kullanımdan kalkmış olarak işaretler. İşaretli öğe kullanıldığında PHP'nin kendi kullanımdan kaldırma mekanizmasıyla aynı şekilde uyarı üretilir; tek fark, hata kodunun E_DEPRECATED değil E_USER_DEPRECATED olmasıdır. message ve since argümanları mesaja eklenir; since değeri PHP tarafından doğrulanmaz.

php
<?php
#[\Deprecated(message: 'yerine yeniHesapla() kullanın', since: '2.0')]
function eskiHesapla(): int
{
    return 1;
}

echo eskiHesapla(), "\n";
/* çıktı:
Deprecated: Function eskiHesapla() is deprecated since 2.0, yerine yeniHesapla() kullanın in /yol/ornek.php on line 8
1
*/

Bu uyarıların ekranda görünüp görünmemesi hata raporlama ayarına bağlıdır. Uyarıları yakalayıp günlüğe yazmak veya istisnaya çevirmek için error_reporting ve hata seviyeleri yazısına bakın.

#[\SensitiveParameter]: Şifreyi Yığın İzinden Gizlemek

Bir istisna fırlatıldığında yığın izi (stack trace) fonksiyon argümanlarını da içerebilir. Şifre, API anahtarı veya kart numarası gibi değerler böylece günlük dosyalarına sızabilir. Parametreye #[\SensitiveParameter] eklediğinizde, yığın izinde gerçek değer yerine bir SensitiveParameterValue nesnesi görünür.

php
<?php
function girisYap(string $kullanici, #[\SensitiveParameter] string $sifre): void
{
    throw new RuntimeException('Giriş başarısız');
}

try {
    girisYap('ali', 'gizli123');
} catch (RuntimeException $e) {
    echo $e->getTraceAsString(), "\n";
}
/* çıktı:
#0 /yol/ornek.php(8): girisYap('ali', Object(SensitiveParameterValue))
#1 {main}
*/

#[\NoDiscard]: Dönüş Değerinin Unutulmasını Önlemek

PHP 8.5 ile gelen bu öznitelik, dönüş değeri kullanılmadığında uyarı üretir. Dönüş değerini kontrol etmemenin büyük olasılıkla hata olduğu fonksiyonlar için tasarlanmıştır. Değeri bilerek yok saymak istediğinizde yine PHP 8.5 ile gelen (void) dönüşümünü kullanırsınız. Bu bölümdeki kod PHP 8.5 veya üstünü gerektirir; kılavuz, özniteliğin eski sürümlerde de yazılabileceğini ama hiçbir etki yapmayacağını, (void) dönüşümünün ise 8.5 öncesinde desteklenmediğini, bu durumda $_ gibi bir değişkene atamanın önerildiğini belirtir.

php
<?php
#[\NoDiscard]
function bakiyeDus(int $tutar): bool
{
    return $tutar <= 100;
}

bakiyeDus(50);
// Warning: The return value of function bakiyeDus() should either be used or intentionally ignored by casting it as (void) in /yol/ornek.php on line 8

$sonuc = bakiyeDus(50);   // uyarı yok: değer kullanıldı
(void) bakiyeDus(50);     // uyarı yok: bilerek yok sayıldı
echo "bitti\n";           // çıktı: bitti

#[\AllowDynamicProperties] ise farklı bir ihtiyaca cevap verir: PHP 8.2'den beri tanımlanmamış bir özelliğe değer atamak Deprecated: Creation of dynamic property A::$x is deprecated uyarısı üretir. Bu öznitelikle işaretlenen sınıflarda ve onların alt sınıflarında bu uyarı çıkmaz. Kılavuz yine de özelliği açıkça tanımlamayı önerir ve özniteliği son çare olarak görür.

Uygulamalı Mini Örnek: Özniteliklerle Form Doğrulama

Şimdi kendi özniteliklerimizle küçük bir doğrulayıcı yazalım. Her kural bir öznitelik sınıfı olsun ve ortak bir Kural arayüzünü uygulasın. Doğrulayıcı, nesnenin her özelliğindeki kuralları IS_INSTANCEOF ile tek seferde bulsun, örneklesin ve hataları toplasın. Uzunluk kuralı IS_REPEATABLE ile işaretlendiği için aynı özelliğe birden fazla kez uygulanabilir.

php
<?php
interface Kural
{
    public function denetle(mixed $deger): ?string;
}

#[Attribute(Attribute::TARGET_PROPERTY)]
final class Zorunlu implements Kural
{
    public function denetle(mixed $deger): ?string
    {
        return ($deger === null || $deger === '') ? 'boş bırakılamaz' : null;
    }
}

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
final class Uzunluk implements Kural
{
    public function __construct(public int $min = 0, public int $max = PHP_INT_MAX) {}

    public function denetle(mixed $deger): ?string
    {
        $n = mb_strlen((string) $deger);
        return ($n < $this->min || $n > $this->max)
            ? "uzunluğu {$this->min}-{$this->max} arasında olmalı"
            : null;
    }
}

final class KayitFormu
{
    public function __construct(
        #[Zorunlu, Uzunluk(min: 3, max: 20)]
        public string $kullaniciAdi,
        #[Zorunlu]
        public string $eposta,
    ) {}
}

function dogrula(object $nesne): array
{
    $hatalar = [];
    foreach ((new ReflectionObject($nesne))->getProperties() as $ozellik) {
        $kurallar = $ozellik->getAttributes(Kural::class, ReflectionAttribute::IS_INSTANCEOF);
        foreach ($kurallar as $attr) {
            $mesaj = $attr->newInstance()->denetle($ozellik->getValue($nesne));
            if ($mesaj !== null) {
                $hatalar[] = $ozellik->getName() . ' ' . $mesaj;
            }
        }
    }
    return $hatalar;
}

print_r(dogrula(new KayitFormu('al', '')));
/* çıktı:
Array
(
    [0] => kullaniciAdi uzunluğu 3-20 arasında olmalı
    [1] => eposta boş bırakılamaz
)
*/
var_dump(dogrula(new KayitFormu('ayse', 'ayse@example.com')) === []); // çıktı: bool(true)

Bu örnekte öznitelikler kurucuda tanıtılan özelliklere yazıldı; kurucu özellik tanıtımında öznitelik hem parametreye hem de özelliğe uygulanır, bu yüzden ReflectionProperty onları görür. Doğrulama kuralları sınıfın içinde, ilgili alanın hemen yanında duruyor ve doğrulayıcı yeni bir kural sınıfı eklendiğinde hiç değişmeden çalışmaya devam ediyor. Gerçek bir uygulamada e-posta biçimini denetlemek için kendi kuralınızın içinde filter_var() kullanabilirsiniz; ayrıntılar filter_var yazısında.

Sık Yapılan Hatalar

  • Öznitelik sınıfına #[Attribute] eklemeyi unutmak: Sınıf var olsa bile işaretli değilse newInstance() hata verir. Kendi öznitelik sınıflarınızın hepsine #[Attribute] ekleyin.
  • Ad alanında baştaki ters bölüyü unutmak: namespace App; altında #[Override] yazmak App\Override arar. Hazır öznitelikleri #[\Override] biçiminde yazın.
  • Argümanda değişken veya fonksiyon çağrısı kullanmak: Argümanlar yalnızca sabit ifade olabilir; #[Rota($yol)] geçersizdir.
  • Hataların erken çıkacağını sanmak: Kendi özniteliklerinizdeki hedef ve tekrar hataları yalnızca newInstance() çağrıldığında görülür. Testlerinizde öznitelikleri gerçekten örnekleyin.
  • Özniteliğin kendi başına bir şey yapmasını beklemek: Kullanıcı tanımlı bir öznitelik, onu okuyan kod olmadıkça hiçbir davranış değiştirmez.

Sık Sorulan Sorular

Öznitelikler hangi PHP sürümünden itibaren var?

PHP 8.0 ile geldi. Hazır özniteliklerin her biri farklı sürümlerde eklendi; yukarıdaki tabloda kılavuzdaki sürümler yer alıyor. #[\NoDiscard] ve #[\DelayedTargetValidation] PHP 8.5 gerektirir.

Öznitelikler kalıtımla alt sınıfa geçer mi?

Kılavuz, AllowDynamicProperties sayfasında özniteliklerin kendilerinin kalıtılmadığını, ancak bu özniteliğin etkisinin alt sınıflara geçtiğini belirtir. Kendi özniteliklerinizi okurken üst sınıftakileri de görmek istiyorsanız getParentClass() ile üst sınıfa çıkıp onun özniteliklerini de okumanız gerekir.

Öznitelik okumak performansı etkiler mi?

Öznitelikler yalnızca Reflection ile okunduğunda işlenir. Bu yazıda bir ölçüm yapılmadı; sık çalışan kodda okunan sonuçları bir dizide önbelleğe almak, aynı Reflection işlemini her istekte tekrar etmekten kaçınmanın basit bir yoludur.

Hangi öğelere öznitelik eklenebilir?

Sınıflar (enum dahil), fonksiyonlar, metotlar, parametreler, özellikler, sınıf sabitleri ve enum case'leri. PHP 8.5 ile sınıf dışında const ile tanımlanan sabitler de bu listeye katıldı; define() ile tanımlanan sabitlere öznitelik eklenemez.

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 · PHP enum kullanımı · PHP kalıtım, abstract ve interface · PHP 8.5 yenilikleri · PHP hata seviyeleri