Dziedziczenie i klasy abstrakcyjne

🎯 Po tej lekcji rozszerzysz klasę przez extends, wywołasz parent::__construct i zdefiniujesz klasę abstrakcyjną z metodami abstrakcyjnymi.

Wyobraź sobie sklep z pojazdami: wszystkie pojazdy mają markę i rok produkcji, ale samochody mają dodatkowo liczbę drzwi, a motocykle — typ silnika. Kopiowanie właściwości do każdej klasy osobno to strata czasu i źródło błędów. Dziedziczenie pozwala zdefiniować wspólne cechy raz — w klasie bazowej — i rozszerzyć je w klasach pochodnych.

extends — rozszerzanie klasy

Słowo kluczowe extends sprawia, że podklasa dziedziczy wszystkie właściwości i metody klasy nadrzędnej:

<?php
declare(strict_types=1);

class Pojazd
{
    public function __construct(
        protected string $marka,
        protected int    $rokProdukcji,
    ) {}

    public function podstawoweInfo(): string
    {
        return $this->marka . ' (' . $this->rokProdukcji . ')';
    }
}

class Samochod extends Pojazd
{
    public function __construct(
        string $marka,
        int    $rokProdukcji,
        private int $liczbaDrzwi,
    ) {
        parent::__construct($marka, $rokProdukcji);
    }

    public function info(): string
    {
        return $this->podstawoweInfo() . ', drzwi: ' . $this->liczbaDrzwi;
    }
}

class Motocykl extends Pojazd
{
    public function __construct(
        string $marka,
        int    $rokProdukcji,
        private string $typSilnika,
    ) {
        parent::__construct($marka, $rokProdukcji);
    }

    public function info(): string
    {
        return $this->podstawoweInfo() . ', silnik: ' . $this->typSilnika;
    }
}

$auto = new Samochod('Toyota', 2020, 4);
$moto = new Motocykl('Honda', 2019, 'V2');

echo $auto->info(); // wynik: Toyota (2020), drzwi: 4
echo $moto->info(); // wynik: Honda (2019), silnik: V2

parent:: — wywołanie kodu klasy nadrzędnej

Gdy podklasa definiuje własny konstruktor, musi zadbać o inicjalizację właściwości z klasy nadrzędnej. Robi to przez parent::__construct(...):

parent::__construct($marka, $rokProdukcji);

Operator parent:: możesz też użyć w zwykłych metodach, żeby wywołać metodę nadrzędną i rozszerzyć jej działanie:

<?php
declare(strict_types=1);

class Pojazd
{
    public function __construct(protected string $marka) {}

    public function opisz(): string
    {
        return 'Pojazd: ' . $this->marka;
    }
}

class ElektrycznySamochod extends Pojazd
{
    public function __construct(
        string $marka,
        private int $zasiegKm,
    ) {
        parent::__construct($marka);
    }

    public function opisz(): string
    {
        return parent::opisz() . ', zasięg: ' . $this->zasiegKm . ' km';
    }
}

$tesla = new ElektrycznySamochod('Tesla', 500);
echo $tesla->opisz(); // wynik: Pojazd: Tesla, zasięg: 500 km

Nadpisywanie metod (override)

Podklasa może nadpisać metodę z klasy nadrzędnej, deklarując ją pod tą samą nazwą. PHP wywoła wersję z podklasy:

<?php
declare(strict_types=1);

class Powiadomienie
{
    public function wyslij(string $wiadomosc): void
    {
        echo 'Powiadomienie: ' . $wiadomosc;
    }
}

class PowiadomienieEmail extends Powiadomienie
{
    public function __construct(private string $adresEmail) {}

    public function wyslij(string $wiadomosc): void
    {
        echo 'E-mail do ' . $this->adresEmail . ': ' . $wiadomosc;
    }
}

class PowiadomienieSMS extends Powiadomienie
{
    public function __construct(private string $numerTelefonu) {}

    public function wyslij(string $wiadomosc): void
    {
        echo 'SMS na ' . $this->numerTelefonu . ': ' . $wiadomosc;
    }
}

$email = new PowiadomienieEmail('[email protected]');
$sms   = new PowiadomienieSMS('+48500123456');

$email->wyslij('Twoje zamówienie jest gotowe.'); // wynik: E-mail do [email protected]: Twoje zamówienie jest gotowe.
$sms->wyslij('Twoje zamówienie jest gotowe.');   // wynik: SMS na +48500123456: Twoje zamówienie jest gotowe.

Klasy abstrakcyjne

Klasa abstrakcyjna to klasa, której nie można instancjonować bezpośrednio (nie możesz zrobić new NazwaKlasy()). Służy wyłącznie jako baza dla podklas. Deklarujesz ją słowem abstract:

<?php
declare(strict_types=1);

abstract class Ksztalt
{
    // metoda abstrakcyjna — brak ciała, musi być zaimplementowana w podklasie
    abstract public function pole(): float;

    // metoda konkretna — dziedziczy się bez zmian (lub można nadpisać)
    public function opisz(): string
    {
        return get_class($this) . ': pole = ' . $this->pole();
    }
}

class Prostokat extends Ksztalt
{
    public function __construct(
        private float $szerokosc,
        private float $wysokosc,
    ) {}

    public function pole(): float
    {
        return $this->szerokosc * $this->wysokosc;
    }
}

class Trojkat extends Ksztalt
{
    public function __construct(
        private float $podstawa,
        private float $wysokosc,
    ) {}

    public function pole(): float
    {
        return ($this->podstawa * $this->wysokosc) / 2;
    }
}

$p = new Prostokat(4.0, 3.0);
$t = new Trojkat(6.0, 4.0);

echo $p->opisz(); // wynik: Prostokat: pole = 12
echo $t->opisz(); // wynik: Trojkat: pole = 12

// new Ksztalt(); // Fatal error: Cannot instantiate abstract class Ksztalt
⚠️ Fatal Error przy instancjonowaniu abstrakcyjnej: Próba new Ksztalt() kończy się błędem wykonania (Fatal Error), nie ostrzeżeniem. Klasa abstrakcyjna to projekt, nie gotowy produkt.

Kiedy używać dziedziczenia?

Dziedziczenie sprawdza się, gdy klasy łączy relacja „jest rodzajem" (ang. is-a):

  • Samochod jest rodzajem Pojazd
  • Prostokat jest rodzajem Ksztalt
  • PowiadomienieEmail jest rodzajem Powiadomienie

Unikaj dziedziczenia dla relacji „ma" (ang. has-a) — tu lepszy jest inny wzorzec (kompozycja), ale to już temat na osobny moduł.

💡 PHP pozwala dziedziczyć tylko po jednej klasie. W odróżnieniu od niektórych innych języków, PHP nie ma wielokrotnego dziedziczenia klas. Jeśli potrzebujesz współdzielić kod z wielu źródeł, użyj interfejsów (lekcja 5) lub traitów (lekcja 6).

W następnej lekcji poznasz interfejsy — lżejszą formę kontraktu, gdzie jedna klasa może zobowiązać się do wielu niezależnych umów.

Sprawdź się

Co się stanie, gdy spróbujesz wywołać new na klasie zadeklarowanej jako abstract?

Do czego służy parent::__construct() w konstruktorze podklasy?

Czym jest metoda abstrakcyjna?

Ćwiczenie

Stwórz abstrakcyjną klasę Ksztalt z metodą abstrakcyjną pole(): float i metodą opisz(): string (zwraca 'Pole: X'). Następnie utwórz klasy Prostokat (szerokosc, wysokosc) i Kolo (promien, użyj M_PI). Utwórz obiekty obu klas i wywołaj na nich opisz().

Pokaż rozwiązanie
<?php
declare(strict_types=1);

abstract class Ksztalt
{
    abstract public function pole(): float;

    public function opisz(): string
    {
        return 'Pole: ' . round($this->pole(), 2);
    }
}

class Prostokat extends Ksztalt
{
    public function __construct(
        private float $szerokosc,
        private float $wysokosc,
    ) {}

    public function pole(): float
    {
        return $this->szerokosc * $this->wysokosc;
    }
}

class Kolo extends Ksztalt
{
    public function __construct(
        private float $promien,
    ) {}

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

$p = new Prostokat(4.0, 3.0);
$k = new Kolo(5.0);

echo $p->opisz(); // wynik: Pole: 12
echo $k->opisz(); // wynik: Pole: 78.54