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
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
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?
private ogranicza dostęp wyłącznie do wnętrza klasy, w której właściwość lub metoda jest zdefiniowana. Kod poza klasą — nawet podklasa — nie może odczytać ani zmienić prywatnego pola.
Czym różni się protected od private?
protected działa jak private, ale dodatkowo daje dostęp klasom rozszerzającym (podklasom). Przydaje się gdy chcesz, żeby dziedzicząca klasa mogła używać właściwości, ale kod poza hierarchią już nie.
Po co stosuje się settery z walidacją?
Setter to metoda, która przyjmuje nową wartość i może ją sprawdzić zanim przypisze do prywatnej właściwości. Dzięki temu obiekt nigdy nie znajdzie się w nieprawidłowym stanie — każda zmiana przechodzi przez kontrolę.
Ć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