Widoczność i enkapsulacja

🎯 Po tej lekcji użyjesz public, private i protected oraz napiszesz gettery i settery chroniące stan klasy.

W poprzednich lekcjach wszystkie właściwości były public — każdy kod mógł je odczytać i zmienić bez żadnej kontroli. W realnych aplikacjach to problem: nic nie stoi na przeszkodzie, żeby ktoś ustawił saldo konta bankowego na wartość ujemną, albo wiek osoby na tysiąc lat. Enkapsulacja (ang. encapsulation) to technika chowania danych wewnątrz klasy i udostępniania ich tylko w kontrolowany sposób.

Trzy poziomy widoczności

PHP definiuje trzy modyfikatory:

| Modyfikator | Widoczny z klasy | Widoczny z podklasy | Widoczny z zewnątrz | |---|:---:|:---:|:---:| | public | ✓ | ✓ | ✓ | | protected | ✓ | ✓ | ✗ | | private | ✓ | ✗ | ✗ |

<?php
declare(strict_types=1);

class Przyklad
{
    public string    $pub  = 'publiczna';
    protected string $prot = 'chroniona';
    private string   $priv = 'prywatna';

    public function pokazWszystkie(): void
    {
        echo $this->pub;   // OK — dostęp z wnętrza klasy
        echo $this->prot;  // OK
        echo $this->priv;  // OK
    }
}

$obj = new Przyklad();
echo $obj->pub;   // wynik: publiczna
// echo $obj->prot; // Fatal error: Cannot access protected property
// echo $obj->priv; // Fatal error: Cannot access private property

Enkapsulacja w praktyce — KontoBankowe

Wyobraź sobie konto bankowe. Saldo nie powinno być dostępne bezpośrednio — każda zmiana musi przejść przez logikę (np. sprawdzenie pokrycia):

<?php
declare(strict_types=1);

class KontoBankowe
{
    private float $saldo;

    public function __construct(float $saldoPoczatkowe = 0.0)
    {
        if ($saldoPoczatkowe < 0.0) {
            throw new \InvalidArgumentException('Saldo początkowe nie może być ujemne.');
        }
        $this->saldo = $saldoPoczatkowe;
    }

    public function wplac(float $kwota): void
    {
        if ($kwota <= 0.0) {
            throw new \InvalidArgumentException('Kwota wpłaty musi być dodatnia.');
        }
        $this->saldo += $kwota;
    }

    public function wyplac(float $kwota): void
    {
        if ($kwota <= 0.0) {
            throw new \InvalidArgumentException('Kwota wypłaty musi być dodatnia.');
        }
        if ($kwota > $this->saldo) {
            throw new \RuntimeException('Brak środków na koncie.');
        }
        $this->saldo -= $kwota;
    }

    public function getSaldo(): float
    {
        return $this->saldo;
    }
}

$konto = new KontoBankowe(500.0);
$konto->wplac(200.0);
$konto->wyplac(100.0);
echo $konto->getSaldo(); // wynik: 600

// $konto->saldo = -9999; // Fatal error: Cannot access private property

Teraz nikt z zewnątrz nie może bezpośrednio zmienić salda. Każda operacja przechodzi przez metodę, która sprawdza poprawność.

Gettery i settery

Getter to metoda zwracająca wartość prywatnej właściwości. Setter to metoda przyjmująca nową wartość — i mogąca ją walidować przed przypisaniem:

<?php
declare(strict_types=1);

class Osoba
{
    private string $imie;
    private int    $wiek;

    public function __construct(string $imie, int $wiek)
    {
        $this->setImie($imie);
        $this->setWiek($wiek);
    }

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

    public function setImie(string $imie): void
    {
        $imie = trim($imie);
        if ($imie === '') {
            throw new \InvalidArgumentException('Imię nie może być puste.');
        }
        $this->imie = $imie;
    }

    public function getWiek(): int
    {
        return $this->wiek;
    }

    public function setWiek(int $wiek): void
    {
        if ($wiek < 0 || $wiek > 150) {
            throw new \InvalidArgumentException("Nieprawidłowy wiek: $wiek");
        }
        $this->wiek = $wiek;
    }
}

$osoba = new Osoba('Ola', 25);
echo $osoba->getImie(); // wynik: Ola
echo $osoba->getWiek(); // wynik: 25

$osoba->setWiek(30);
echo $osoba->getWiek(); // wynik: 30

// $osoba->setWiek(-5); // rzuci InvalidArgumentException
💡 Czy zawsze potrzebujesz getterów i setterów? Nie. Jeśli właściwość jest naprawdę publiczna i nie wymaga walidacji, public jest prostsze. Nadmierne owijanie każdego pola w getter/setter bez powodu to tzw. anemic domain model — wystarczający znak, że klasa nie ma żadnej logiki.

Właściwości obliczane — getter bez settera

Czasem getter zwraca wartość obliczoną z innych właściwości, a setter nie ma sensu:

<?php
declare(strict_types=1);

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

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

    // właściwość obliczana — nie ma settera
    public function getPole(): float
    {
        return $this->szerokosc * $this->wysokosc;
    }

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

$r = new Prostokat(5.0, 3.0);
echo $r->getPole();  // wynik: 15
echo $r->getObwod(); // wynik: 16

protected — dla podklas

protected przydaje się w dziedziczeniu (lekcja 4): właściwość jest prywatna dla świata zewnętrznego, ale dostępna dla klas dziedziczących.

<?php
declare(strict_types=1);

class Pojazd
{
    protected int $predkoscMax;

    public function __construct(int $predkoscMax)
    {
        $this->predkoscMax = $predkoscMax;
    }
}

class Motocykl extends Pojazd
{
    public function czyPrzekraczaLimit(): bool
    {
        // dostęp do protected z podklasy — OK
        return $this->predkoscMax > 200;
    }
}

$m = new Motocykl(250);
echo $m->czyPrzekraczaLimit(); // wynik: 1 (true)
// echo $m->predkoscMax; // Fatal error: Cannot access protected property
⚠️ Zasada najmniejszego uprzywilejowania: Domyślnie deklaruj właściwości jako private. Zmieniaj na protected tylko gdy podklasy naprawdę potrzebują dostępu, a na public tylko gdy dostęp z zewnątrz jest świadomą decyzją projektową.

Dobra enkapsulacja sprawia, że wnętrze klasy możesz dowolnie zmieniać — dopóki publiczny interfejs (nazwy i sygnatury publicznych metod) pozostaje bez zmian, reszta kodu nadal działa. W kolejnej lekcji rozwiniesz hierarchie klas przez dziedziczenie.

Sprawdź się

Skąd można odczytać właściwość oznaczoną private?

Czym różni się protected od private?

Po co stosuje się settery z walidacją?

Ćwiczenie

Napisz klasę Temperatura z prywatną właściwością float $stopnieCelsjusza. Dodaj konstruktor ustawiający temperaturę (wyrzuć wyjątek gdy < -273.15). Dodaj getter getCelsjusz(): float oraz setter setCelsjusz(float $t): void z tą samą walidacją. Dodaj metodę getFahrenheit(): float (wzór: C * 9/5 + 32). Przetestuj kilkoma wartościami.

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

class Temperatura
{
    private float $stopnieCelsjusza;

    public function __construct(float $stopnieCelsjusza)
    {
        $this->setCelsjusz($stopnieCelsjusza);
    }

    public function getCelsjusz(): float
    {
        return $this->stopnieCelsjusza;
    }

    public function setCelsjusz(float $t): void
    {
        if ($t < -273.15) {
            throw new \InvalidArgumentException("Temperatura poniżej zera absolutnego: $t");
        }
        $this->stopnieCelsjusza = $t;
    }

    public function getFahrenheit(): float
    {
        return $this->stopnieCelsjusza * 9 / 5 + 32;
    }
}

$t = new Temperatura(100.0);
echo $t->getCelsjusz();   // wynik: 100
echo $t->getFahrenheit(); // wynik: 212

$t->setCelsjusz(-40.0);
echo $t->getCelsjusz();   // wynik: -40
echo $t->getFahrenheit(); // wynik: -40