Interfejsy

🎯 Po tej lekcji zdefiniujesz interfejs, zaimplementujesz go w klasie i zrozumiesz, czym różni się od dziedziczenia klasy abstrakcyjnej.

Klasa abstrakcyjna to dobry sposób na współdzielenie kodu — ale ma jedno ograniczenie: PHP pozwala dziedziczyć tylko po jednej klasie. Co zrobić, gdy klasa powinna spełniać kilka niezależnych kontraktów? Na przykład Faktura powinna umieć być drukowana i jednocześnie eksportowana do PDF. Tu wkraczają interfejsy.

Czym jest interfejs?

Interfejs to czysta lista wymagań — deklarujesz w nim metody bez żadnej implementacji. Klasa, która implementuje interfejs, zobowiązuje się dostarczyć wszystkie te metody.

Analogia: interfejs to formularz umowy. Opisuje obowiązki strony (klasy), ale nie mówi, jak je wypełnić.

<?php
declare(strict_types=1);

interface Drukowalna
{
    public function drukuj(): void;
}

Interfejs:

  • definiujesz słowem interface,
  • metody mają sygnatury (nazwę, parametry, typ zwracany), ale brak ciała (klamr {}),
  • metody są niejawnie public — nie musisz pisać public, ale możesz,
  • nie może mieć zwykłych właściwości (możliwe są tylko stałe).

implements — klasa spełniająca kontrakt

Klasa deklaruje, że implementuje interfejs słowem implements. Musi wtedy dostarczyć implementację wszystkich metod interfejsu:

<?php
declare(strict_types=1);

interface Drukowalna
{
    public function drukuj(): void;
}

class Faktura implements Drukowalna
{
    public function __construct(private string $numer) {}

    public function drukuj(): void
    {
        echo 'Drukuję fakturę: ' . $this->numer;
    }
}

class Raport implements Drukowalna
{
    public function __construct(private string $tytul) {}

    public function drukuj(): void
    {
        echo 'Drukuję raport: ' . $this->tytul;
    }
}

$dokumenty = [
    new Faktura('FV/2024/001'),
    new Raport('Raport sprzedaży Q4'),
];

foreach ($dokumenty as $dok) {
    $dok->drukuj();
}
// wynik: Drukuję fakturę: FV/2024/001
// wynik: Drukuję raport: Raport sprzedaży Q4
💡 Typowanie przez interfejs: Zmienna tablicy $dokumenty trzyma obiekty różnych klas, bo oba spełniają interfejs Drukowalna. PHP pozwala deklarować parametry funkcji jako Drukowalna $dok — taka funkcja przyjmie dowolny obiekt implementujący ten interfejs.

Wiele interfejsów jednocześnie

Jedna klasa może implementować wiele interfejsów — wymień je po przecinku:

<?php
declare(strict_types=1);

interface Drukowalna
{
    public function drukuj(): void;
}

interface Exportowalna
{
    public function eksportuj(string $format): string;
}

interface Archiwizowalna
{
    public function archiwizuj(): void;
}

class Faktura implements Drukowalna, Exportowalna, Archiwizowalna
{
    public function __construct(private string $numer) {}

    public function drukuj(): void
    {
        echo 'Drukuję: ' . $this->numer;
    }

    public function eksportuj(string $format): string
    {
        return $this->numer . '.' . strtolower($format);
    }

    public function archiwizuj(): void
    {
        echo 'Archiwizuję: ' . $this->numer;
    }
}

$f = new Faktura('FV/2024/042');
$f->drukuj();
echo $f->eksportuj('PDF'); // wynik: FV/2024/042.pdf
$f->archiwizuj();

Interfejsy mogą rozszerzać inne interfejsy

Interfejs może rozszerzać inny interfejs (lub kilka) przez extends. Klasa implementująca rozszerzony interfejs musi dostarczyć metody obu:

<?php
declare(strict_types=1);

interface Zapisywalny
{
    public function zapisz(): void;
}

interface Odczytywalny
{
    public function odczytaj(): string;
}

interface Persistowalny extends Zapisywalny, Odczytywalny
{
    // może dodać własne metody lub być "puste" — zbiera obu poprzedników
}

class Dokument implements Persistowalny
{
    private string $tresc = '';

    public function zapisz(): void
    {
        echo 'Zapisano.';
    }

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

Stałe w interfejsach

Interfejs może definiować stałe (słowem const), ale nie właściwości:

<?php
declare(strict_types=1);

interface Statusowy
{
    const AKTYWNY    = 'aktywny';
    const NIEAKTYWNY = 'nieaktywny';

    public function getStatus(): string;
}

class Uzytkownik implements Statusowy
{
    private string $status = self::AKTYWNY;

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

$u = new Uzytkownik();
echo $u->getStatus();         // wynik: aktywny
echo Statusowy::AKTYWNY;      // wynik: aktywny
⚠️ Brak implementacji = błąd Fatal: Jeśli klasa deklaruje implements DanyInterfejs, ale nie dostarcza implementacji którejś z wymaganych metod, PHP zgłosi Fatal Error już przy wczytywaniu klasy.

Interfejs vs klasa abstrakcyjna

| | Interfejs | Klasa abstrakcyjna | |---|---|---| | Implementacja metod | Brak (tylko deklaracje) | Możliwa (część może być konkretna) | | Właściwości | Brak (tylko stałe) | Możliwe | | Ile naraz? | Wiele | Tylko jedna | | Konstruktor | Brak | Możliwy | | Kiedy? | Kontrakt bez implementacji | Wspólna baza z częściową implementacją |

Praktyczna zasada: jeśli chcesz tylko opisać co klasa robi (bez jak), użyj interfejsu. Jeśli chcesz też dostarczyć część gotowego kodu, użyj klasy abstrakcyjnej.

W następnej lekcji poznasz traity — mechanizm pozwalający współdzielić konkretny kod między klasami bez dziedziczenia.

Sprawdź się

Ile interfejsów może jednocześnie implementować jedna klasa PHP?

Jaka jest widoczność metod zadeklarowanych w interfejsie?

Czym różni się interfejs od klasy abstrakcyjnej?

Ćwiczenie

Zdefiniuj interfejs Porównywalny z metodą porownajDo(mixed $inny): int (zwraca ujemną, zero lub dodatnią, jak w usort). Zdefiniuj interfejs Opisywalny z metodą opisz(): string. Utwórz klasę Produkt (nazwa, cena), która implementuje oba interfejsy. porownajDo sortuje po cenie. Posortuj tablicę produktów używając usort i wyświetl nazwy.

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

interface Porownywalny
{
    public function porownajDo(mixed $inny): int;
}

interface Opisywalny
{
    public function opisz(): string;
}

class Produkt implements Porownywalny, Opisywalny
{
    public function __construct(
        public readonly string $nazwa,
        public readonly float  $cena,
    ) {}

    public function porownajDo(mixed $inny): int
    {
        return $this->cena <=> $inny->cena;
    }

    public function opisz(): string
    {
        return $this->nazwa . ' — ' . $this->cena . ' zł';
    }
}

$produkty = [
    new Produkt('Arbuz',  12.99),
    new Produkt('Jabłka',  3.49),
    new Produkt('Chleb',   4.99),
];

usort($produkty, fn(Produkt $a, Produkt $b) => $a->porownajDo($b));

foreach ($produkty as $p) {
    echo $p->opisz(); // wynik kolejno: Jabłka — 3.49 zł / Chleb — 4.99 zł / Arbuz — 12.99 zł
}