PHP'de kalıtım (inheritance), bir sınıfın extends anahtar kelimesiyle başka bir sınıfın public ve protected özelliklerini, metotlarını ve sabitlerini devralmasıdır. Alt sınıf devraldığı metodu aynı adla yeniden tanımlayarak ezebilir (override) ve gerekirse parent::metot() ile üst sınıftaki sürümü çağırabilir. Bir sınıf yalnızca tek bir sınıftan türeyebilir. Abstract sınıf doğrudan örneklenemeyen, alt sınıflara tamamlanması gereken metotlar bırakan yarım bir şablondur. Interface ise yalnızca hangi public metotların bulunması gerektiğini söyleyen bir sözleşmedir ve bir sınıf birden çok interface uygulayabilir.

Bu yazıda extends, parent::, imza uyumluluğu kuralları, abstract, interface, final ve PHP 8.3 ile gelen #[\Override] attribute'unu çalışan örneklerle ele alıyoruz. Sınıf ve nesnenin temellerini henüz görmediyseniz önce sınıf ve nesne yazısını, serinin tamamı için PHP öğrenme rehberini okuyabilirsiniz.

extends ile devralmak ve metodu ezmek

Bir alt sınıf, üst sınıftaki her public ve protected üyeyi olduğu gibi kullanabilir. Aynı adla bir metot yazdığınızda o metot alt sınıfta üst sınıfınkinin yerini alır. Üst sınıfın davranışını tamamen atmak yerine genişletmek istiyorsanız parent:: önekiyle orijinal metodu çağırırsınız. PHP, ezilen metodun üst sürümünü kendiliğinden çalıştırmaz; çağırıp çağırmamak alt sınıfın kararıdır.

php
<?php
class Bildirim
{
    public function __construct(protected string $alici) {}

    public function gonder(string $mesaj): string
    {
        return "[{$this->alici}] $mesaj";
    }

    public function tur(): string
    {
        return 'genel';
    }
}

class SmsBildirim extends Bildirim
{
    public function gonder(string $mesaj): string
    {
        $kisa = mb_substr($mesaj, 0, 19) . '...';
        return 'SMS ' . parent::gonder($kisa);
    }
}

$b = new SmsBildirim('0555 000 00 00');
echo $b->gonder('Siparişiniz kargoya verildi, takip no 123'), PHP_EOL;
// çıktı: SMS [0555 000 00 00] Siparişiniz kargoya...
echo $b->tur(), PHP_EOL;          // çıktı: genel
var_dump($b instanceof Bildirim); // çıktı: bool(true)

SmsBildirim, tur() metodunu hiç yazmadığı hâlde kullanabiliyor, çünkü onu üst sınıftan devraldı. $alici özelliği protected olduğu için alt sınıfın içinden erişilebilir, dışarıdan erişilemez. Üst sınıfın private üyeleri ise alt sınıfa hiç geçmez; alt sınıf aynı adla bir private metot yazarsa bu, tamamen bağımsız yeni bir metot olur. Bu ayrımı görünürlük yazısında örnekle gösteriyoruz.

instanceof operatörü kalıtım zincirini takip eder: bir SmsBildirim nesnesi aynı zamanda bir Bildirim sayılır. Bu sayede Bildirim tipinde parametre bekleyen her fonksiyona alt sınıf nesnesini geçirebilirsiniz.

Kurucu metotlar kendiliğinden zincirlenmez

Kalıtımda en sık gözden kaçan kural kurucularla ilgilidir. Alt sınıf kendi __construct() metodunu tanımlarsa üst sınıfın kurucusu otomatik çalışmaz; parent::__construct() çağrısını sizin yazmanız gerekir. Alt sınıf hiç kurucu tanımlamazsa, üst sınıfınki (private değilse) sıradan bir metot gibi devralınır. Aşağıda üç farklı durumu yan yana görüyorsunuz.

php
<?php
class Temel
{
    protected string $durum;

    public function __construct()
    {
        $this->durum = 'hazır';
        echo "Temel kurucu çalıştı\n";
    }
}

class Hatirlayan extends Temel
{
    public function __construct()
    {
        parent::__construct();
        echo "Hatirlayan kurucu çalıştı\n";
    }
}

class Miras extends Temel {}

class Unutan extends Temel
{
    public function __construct()
    {
        echo "Unutan kurucu çalıştı\n";
    }

    public function durum(): string
    {
        return $this->durum;
    }
}

new Hatirlayan();
new Miras();
echo (new Unutan())->durum();
text
Temel kurucu çalıştı
Hatirlayan kurucu çalıştı
Temel kurucu çalıştı
Unutan kurucu çalıştı

Fatal error: Uncaught Error: Typed property Temel::$durum must not be accessed before initialization in /yol/ornek.php:33
Stack trace:
#0 /yol/ornek.php(39): Unutan->durum()
#1 {main}
  thrown in /yol/ornek.php on line 33

Unutan sınıfı üst kurucuyu çağırmadığı için $durum hiç atanmadı ve okunmaya çalışıldığında hata oluştu. Kurucular, imza uyumluluğu kurallarının dışında tutulur: alt sınıfın kurucusu üst sınıfınkinden tamamen farklı parametreler alabilir, hatta görünürlüğü daraltılabilir.

İmza uyumluluğu: hangi değişikliklere izin var?

İmza uyumluluğu: hangi değişikliklere izin var? Kovaryans ve kontravaryans, Dahili sınıflardan türetirken dönüş tipi uyarısı

Bir metodu ezerken yeni imzanın üst sınıftakiyle uyumlu olması gerekir; değilse PHP 8.0 ve sonrasında ölümcül hata (fatal error) alırsınız. Kılavuz bu kuralı Liskov Yerine Geçme İlkesi (LSP) ile açıklar: alt sınıf nesnesi, üst sınıf nesnesinin beklendiği her yerde sorunsuz çalışabilmelidir. Pratikte şu değişikliklere izin verilir: zorunlu bir parametreyi isteğe bağlı yapmak, sona yalnızca isteğe bağlı yeni parametreler eklemek, görünürlüğü genişletmek (protected metodu public yapmak) ve tipleri varyans kurallarına göre değiştirmek. Parametre kaldırmak veya isteğe bağlı parametreyi zorunlu hâle getirmek uyumsuzdur.

php
<?php
class Rapor
{
    public function olustur(string $bicim = 'pdf'): string
    {
        return "rapor.$bicim";
    }
}

class OzelRapor extends Rapor
{
    public function olustur(string $bicim): string
    {
        return "ozel.$bicim";
    }
}
text
Fatal error: Declaration of OzelRapor::olustur(string $bicim): string must be compatible with Rapor::olustur(string $bicim = 'pdf'): string in /yol/ornek.php on line 12
Stack trace:
#0 {main}

Hata mesajı iki imzayı da yazdığı için neyin uyuşmadığını görmek kolaydır. Burada üst sınıftaki varsayılan değer alt sınıfta kaldırılmış; çözüm $bicim = 'pdf' varsayılanını korumaktır. Kılavuzun bir uyarısı daha var: alt sınıfta parametrenin adını değiştirmek imza hatası sayılmaz, ama çağıran taraf isimli argüman kullanıyorsa çalışma anında Unknown named parameter hatası oluşur. Bu yüzden ezdiğiniz metotlarda parametre adlarını aynı tutun.

Kovaryans ve kontravaryans

Dönüş tiplerinde alt sınıf daha özel bir tip döndürebilir (kovaryans); parametre tiplerinde ise daha genel bir tip kabul edebilir (kontravaryans). Örneğin üst metot Hayvan döndürüyorsa alt metot Kedi döndürebilir, çünkü her kedi bir hayvandır ve üst sınıfa güvenen kod bozulmaz.

php
<?php
class Hayvan
{
    public function __construct(public string $ad) {}
}

class Kedi extends Hayvan {}

interface Barinak
{
    public function sahiplen(string $ad): Hayvan;
}

class KediBarinagi implements Barinak
{
    public function sahiplen(string $ad): Kedi // daha özel dönüş tipi
    {
        return new Kedi($ad);
    }
}

$k = (new KediBarinagi())->sahiplen('Tekir');
echo get_class($k), ' ', $k->ad, PHP_EOL; // çıktı: Kedi Tekir

Parametrelerde ise yön terstir. Üst metot Kedi tipinde parametre bekliyorsa alt metot Hayvan kabul edebilir, çünkü daha fazla değeri kabul etmek eski çağrıları bozmaz. Tersi, yani parametre tipini daraltmak, üst sınıfa güvenerek yazılmış çağrıların bir kısmını geçersiz kılacağı için imza hatası verir. Kılavuz "daha özel tip" kavramını şöyle tanımlar: union tipten bir tip çıkarmak, intersection tipe bir tip eklemek, sınıf tipini alt sınıfıyla değiştirmek veya iterable yerine array ya da Traversable yazmak. Union ve intersection tiplerin kendisi tip bildirimleri yazısında anlatılıyor.

Dahili sınıflardan türetirken dönüş tipi uyarısı

PHP'nin kendi sınıflarından (örneğin DateTime) türetip bir metodu ezdiğinizde de aynı kurallar geçerlidir. PHP 8.1 ile dahili metotların çoğu dönüş tiplerini "geçici" (tentative) olarak bildirmeye başladı. Ezdiğiniz metot uyumlu bir dönüş tipi bildirmezse, hiç dönüş tipi yazmamanız da dahil, bir kullanımdan kaldırma (deprecation) uyarısı alırsınız. Doğru çözüm uyumlu dönüş tipini yazmaktır; eski PHP sürümleriyle uyumluluk yüzünden bunu yapamıyorsanız metoda #[\ReturnTypeWillChange] attribute'unu ekleyerek uyarıyı geçici olarak susturabilirsiniz.

php
<?php
class Tarihim extends DateTime
{
    public function modify(string $modifier)
    {
        return false;
    }
}

echo 'sınıf yüklendi', PHP_EOL;
text
Deprecated: Return type of Tarihim::modify(string $modifier) should either be compatible with DateTime::modify(string $modifier): DateTime, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /yol/ornek.php on line 4
sınıf yüklendi

Bir başka kalıtım kuralı da özelliklerle ilgilidir: üst sınıfta okunup yazılabilen bir özelliği alt sınıfta readonly olarak yeniden bildiremezsiniz; tersi de yasaktır.

Abstract sınıflar ve metotlar

Abstract sınıflar ve metotlar

abstract olarak işaretlenen bir sınıf doğrudan new ile örneklenemez. İçinde en az bir abstract metot (veya PHP 8.4 itibarıyla abstract özellik) bulunan her sınıfın kendisi de abstract olmak zorundadır. Abstract metot yalnızca imzasını ve görünürlüğünü (public veya protected) bildirir, gövde içermez. Abstract sınıftan türeyen somut bir sınıf, bütün abstract metotları normal imza uyumluluğu kurallarına uyarak tanımlamalıdır.

Abstract sınıfın interface'ten farkı, ortak kodu da taşıyabilmesidir. Aşağıdaki örnekte ozet() metodu bütün şekiller için aynı çalışıyor, ama alanın nasıl hesaplanacağını her alt sınıf kendisi belirliyor. Bu kalıba "şablon metot" (template method) da denir.

php
<?php
abstract class Sekil
{
    abstract public function alan(): float;

    public function ozet(): string
    {
        return $this::class . ' alanı: ' . round($this->alan(), 2);
    }
}

class Daire extends Sekil
{
    public function __construct(private float $r) {}

    public function alan(): float
    {
        return M_PI * $this->r ** 2;
    }
}

class Dikdortgen extends Sekil
{
    public function __construct(private float $en, private float $boy) {}

    public function alan(): float
    {
        return $this->en * $this->boy;
    }
}

foreach ([new Daire(2), new Dikdortgen(3, 4.5)] as $s) {
    echo $s->ozet(), PHP_EOL;
}
/*
Daire alanı: 12.57
Dikdortgen alanı: 13.5
*/

try {
    new Sekil();
} catch (Error $e) {
    echo $e->getMessage(), PHP_EOL; // çıktı: Cannot instantiate abstract class Sekil
}

Alt sınıf, abstract metodun imzasında olmayan isteğe bağlı parametreler ekleyebilir; bu da yukarıdaki uyumluluk kuralının bir sonucudur. PHP 8.4 ile abstract sınıflar abstract public string $ad { get; } biçiminde abstract özellik de bildirebiliyor; bu söz dizimi property hooks ile birlikte geldiği için ayrıntısını property hooks yazısına bırakıyoruz.

Interface: sınıfın uyması gereken sözleşme

Interface, interface anahtar kelimesiyle tanımlanır ve yalnızca metot imzaları içerir; gövde yazılmaz. Interface içinde bildirilen bütün metotlar public olmak zorundadır. Bir sınıf implements ile bir veya virgülle ayırarak birden çok interface uygulayabilir ve her birindeki bütün metotları tanımlamak zorundadır. Kılavuz, interface'lerin iki amacını öne çıkarır: farklı sınıfların birbirinin yerine kullanılabilmesi (ör. farklı ödeme sağlayıcıları) ve bir fonksiyonun, nesnenin başka neler yaptığıyla ilgilenmeden yalnızca belli bir davranışı istemesi.

php
<?php
interface Odeme
{
    public function ode(float $tutar): string;
}

interface Iade
{
    public function iadeEt(float $tutar): string;
}

class KrediKarti implements Odeme, Iade
{
    public function ode(float $tutar): string
    {
        return "Kartla $tutar TL ödendi";
    }

    public function iadeEt(float $tutar): string
    {
        return "Karta $tutar TL iade edildi";
    }
}

class Havale implements Odeme
{
    public function ode(float $tutar): string
    {
        return "Havale ile $tutar TL ödendi";
    }
}

function tahsilEt(Odeme $yontem, float $tutar): void
{
    echo $yontem->ode($tutar), PHP_EOL;
    if ($yontem instanceof Iade) {
        echo '  (bu yöntem iade destekliyor)', PHP_EOL;
    }
}

tahsilEt(new KrediKarti(), 250);
tahsilEt(new Havale(), 99.9);
/*
Kartla 250 TL ödendi
  (bu yöntem iade destekliyor)
Havale ile 99.9 TL ödendi
*/

tahsilEt() fonksiyonu yalnızca Odeme sözleşmesini biliyor; yarın yeni bir ödeme yöntemi eklediğinizde bu fonksiyona dokunmanız gerekmez. Interface'i uygulayan sınıf bir metodu atlarsa sınıf derlenirken ölümcül hata oluşur:

php
<?php
interface Odeme
{
    public function ode(float $tutar): string;
    public function iptal(): void;
}

class EksikOdeme implements Odeme
{
    public function ode(float $tutar): string
    {
        return 'tamam';
    }
}
text
Fatal error: Class EksikOdeme contains 1 abstract method and must therefore be declared abstract or implement the remaining method (Odeme::iptal) in /yol/ornek.php on line 8
Stack trace:
#0 {main}

Mesaj, interface metotlarının teknik olarak abstract metot sayıldığını da gösteriyor. Nitekim bir abstract sınıf, bir interface'in yalnızca bir kısmını uygulayıp gerisini alt sınıflarına bırakabilir. Interface'ler başka interface'leri extends ile genişletebilir; sınıfların aksine bir interface birden çok interface'ten türeyebilir. Bir sınıf hem türeyip hem uyguluyorsa sıra önemlidir: önce extends, sonra implements yazılır.

Interface'ler sabit de içerebilir ve bu sabitler sınıf sabitleri gibi çalışır; PHP 8.1 ve sonrasında uygulayan sınıf bu sabitleri ezebilir. PHP 8.4 ile interface'ler özellik de bildirebiliyor (public string $ad { get; }). Kılavuz, interface içinde kurucu tanımlamanın desteklendiğini ama esnekliği ciddi biçimde azalttığı için kesinlikle önerilmediğini belirtir.

final: ezilmeyi ve türetilmeyi engellemek

final önekli bir metot alt sınıfta ezilemez; final bir sınıftan ise hiç türetilemez. PHP 8.1 ile sınıf sabitleri, PHP 8.4 ile özellikler de final olabiliyor. Bir sınıfın davranışının kesinlikle değiştirilmemesi gereken noktalarında, örneğin güvenlik kontrolü yapan bir metotta, final kullanmak niyetinizi açık eder.

php
<?php
class Para
{
    final public function birim(): string
    {
        return 'TRY';
    }
}

class Dolar extends Para
{
    public function birim(): string
    {
        return 'USD';
    }
}
text
Fatal error: Cannot override final method Para::birim() in /yol/ornek.php on line 12
Stack trace:
#0 {main}
php
<?php
final class Ayarlar {}

class OzelAyarlar extends Ayarlar {}
text
Fatal error: Class OzelAyarlar cannot extend final class Ayarlar in /yol/ornek.php on line 4
Stack trace:
#0 {main}

Kılavuza göre PHP 8.0 ve sonrasında private metotlar, kurucu dışında, final olarak bildirilemez; zaten alt sınıfa geçmedikleri için bunun bir anlamı yoktur. Ayrıca private(set) ile bildirilen bir özellik örtük olarak final sayılır.

#[\Override]: ezdiğinizden emin olun

PHP 8.3 ile gelen #[\Override] attribute'u, bir metodun üst sınıftaki veya uygulanan interface'teki bir metodu ezmek amacıyla yazıldığını belirtir. Eğer üst tarafta aynı adlı bir metot yoksa PHP derleme sırasında hata verir. Bu, iki tür hatayı yakalar: metot adını yanlış yazmak ve üst sınıftaki metodun yeniden adlandırılması ya da silinmesi yüzünden alt sınıftaki metodun sessizce "yetim" kalması. PHP 8.5 ile aynı attribute özelliklere de uygulanabiliyor. Kurucu metotta kullanılamaz.

php
<?php
class Depo
{
    public function kaydet(array $veri): bool
    {
        return true;
    }
}

class OnbellekliDepo extends Depo
{
    #[\Override]
    public function kaydet(array $veri): bool // doğru: üst sınıfta var
    {
        return parent::kaydet($veri);
    }

    #[\Override]
    public function kayded(array $veri): bool // yazım hatası
    {
        return false;
    }
}
text
Fatal error: OnbellekliDepo::kayded() has #[\Override] attribute, but no matching parent method exists in /yol/ornek.php on line 19
Stack trace:
#0 {main}

Attribute sözdiziminin genel kuralları ve kendi attribute sınıflarınızı yazmak için attributes yazısına bakabilirsiniz.

Abstract sınıf, interface ve trait karşılaştırması

Abstract sınıf, interface ve trait karşılaştırması: Örneklenebilir mi, Gövdeli metot, Özellik (property), Bir sınıf kaç tane kullanabilir

Üç yapı da kod paylaşımı veya sözleşme için kullanılır ama farklı sorunları çözer. Aşağıdaki tablo temel farkları özetliyor; trait ayrıntıları trait kullanımı yazısında.

Özellikabstract classinterfacetrait
Örneklenebilir miHayırHayırHayır
Gövdeli metotEvetHayırEvet
Özellik (property)EvetPHP 8.4+ hook bildirimiEvet
Bir sınıf kaç tane kullanabilirBir (extends)Birden çok (implements)Birden çok (use)
Tip olarak kullanılabilir miEvetEvetHayır
Metot görünürlüğüpublic veya protected abstractYalnızca publicSerbest

Sık yapılan hatalar

  • parent::__construct() çağrısını unutmak: Alt sınıf kurucu tanımlarsa üst kurucu çalışmaz ve üst sınıfın hazırladığı özellikler başlatılmamış kalır.
  • Ezilen metotta varsayılan değeri silmek: İsteğe bağlı parametreyi zorunlu yapmak "must be compatible" ölümcül hatasına yol açar.
  • Parametre adını değiştirmek: İmza hatası vermez ama isimli argümanlarla çağrıldığında çalışma anında hata üretir.
  • Görünürlüğü daraltmak: Üst sınıfta public olan bir metodu alt sınıfta protected yapamazsınız; yalnızca genişletebilirsiniz.
  • İki sınıftan türemeye çalışmak: PHP'de çoklu kalıtım yoktur. Ortak davranışı interface ile tanımlayıp kodu trait ile paylaşmak yaygın bir çözümdür.
  • extends ve implements sırasını karıştırmak: Doğru yazım class A extends B implements C biçimindedir.

Sık Sorulan Sorular

PHP'de çoklu kalıtım var mı?

Hayır. Bir sınıf extends ile yalnızca bir sınıftan türeyebilir. Buna karşılık istediği kadar interface uygulayabilir ve birden çok trait kullanabilir.

Abstract sınıf mı interface mi kullanmalıyım?

Yalnızca bir sözleşme tanımlıyorsanız ve farklı hiyerarşilerdeki sınıfların bu sözleşmeye uymasını istiyorsanız interface daha esnektir. Alt sınıflar arasında ortak kod ve durum (özellik) paylaşmak istiyorsanız abstract sınıf uygundur. İkisi birlikte de kullanılabilir: abstract sınıf bir interface'i kısmen uygulayabilir.

Üst sınıfın private metodunu alt sınıfta çağırabilir miyim?

Hayır. Private üyeler yalnızca tanımlandıkları sınıfta erişilebilir. Alt sınıfların kullanmasını istiyorsanız metodu protected yapın.

Interface içinde özellik tanımlanabilir mi?

PHP 8.4 ve sonrasında evet, ama yalnızca okunabilir (get), yazılabilir (set) veya ikisi birden olarak bildirilen public özellikler. Daha eski sürümlerde interface yalnızca metot ve sabit içerebilir.

Bir interface birden çok interface'ten türeyebilir mi?

Evet. Sınıfların aksine interface'ler extends ile virgülle ayrılmış birden çok interface'i genişletebilir. Böyle bir interface'i uygulayan sınıf, zincirdeki bütün interface'lerin metotlarını tanımlamak zorundadır.

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 · Sınıf ve nesne · Trait kullanımı · Görünürlük ve readonly · static ve late static binding · Attributes