Lekcja 26 (OOP 6): Dziedziczenie (extends), Nadpisywanie Metod, Słowo Kluczowe final
Witaj w szóstej lekcji naszego modułu o Programowaniu Obiektowym w PHP! W poprzednich lekcjach poznaliśmy podstawy OOP, nauczyliśmy się tworzyć klasy i obiekty, zarządzać dostępem do ich składowych za pomocą modyfikatorów dostępu oraz korzystać ze składowych statycznych. Dzisiaj zajmiemy się jednym z najważniejszych i najpotężniejszych mechanizmów OOP: dziedziczeniem (inheritance). Dziedziczenie pozwala na tworzenie nowych klas (nazywanych klasami potomnymi lub podklasami) na bazie istniejących klas (nazywanych klasami bazowymi, nadrzędnymi lub nadklasami), co promuje reużywalność kodu i tworzenie hierarchii klas.
Omówimy, jak w PHP implementuje się dziedziczenie za pomocą słowa kluczowego extends
, jak klasy potomne dziedziczą właściwości i metody po klasach bazowych, jak można nadpisywać (override) odziedziczone metody, aby dostosować ich zachowanie, oraz jak używać słowa kluczowego parent::
do wywoływania metod z klasy nadrzędnej. Na koniec przyjrzymy się słowu kluczowemu final
, które pozwala zapobiegać dziedziczeniu klas lub nadpisywaniu metod.
Czym Jest Dziedziczenie?
Dziedziczenie to mechanizm, który pozwala jednej klasie (klasie potomnej) przejmować (dziedziczyć) właściwości i metody z innej klasy (klasy bazowej). Klasa potomna może następnie dodawać własne, unikalne właściwości i metody, a także modyfikować (nadpisywać) zachowanie odziedziczonych metod. Tworzy to relację "jest rodzajem" (is-a relationship) – na przykład, jeśli mamy klasę Pojazd
i klasę Samochod
, która dziedziczy po Pojazd
, to możemy powiedzieć, że "Samochód jest rodzajem Pojazdu".
Główne korzyści z dziedziczenia:
- Reużywalność kodu (Code Reusability): Wspólne właściwości i metody mogą być zdefiniowane raz w klasie bazowej i automatycznie dostępne we wszystkich klasach potomnych. Unikamy w ten sposób powielania kodu.
- Organizacja kodu i tworzenie hierarchii: Dziedziczenie pozwala na tworzenie logicznych hierarchii klas, odzwierciedlających relacje między różnymi typami obiektów w systemie. Na przykład:
Zwierze -> Ssak -> Pies -> Labrador
. - Polimorfizm (Polymorphism): Dziedziczenie jest podstawą polimorfizmu (który omówimy szczegółowo w późniejszej lekcji), czyli zdolności obiektów różnych klas do reagowania na to samo wywołanie metody w sposób specyficzny dla swojej klasy.
- Rozszerzalność (Extensibility): Łatwo jest dodawać nowe funkcjonalności poprzez tworzenie nowych klas potomnych, które dziedziczą istniejące cechy i dodają własne.
W PHP klasa może dziedziczyć tylko po jednej klasie bazowej – PHP nie wspiera wielokrotnego dziedziczenia klas (choć pewne aspekty wielokrotnego dziedziczenia można osiągnąć za pomocą cech – traits, o których powiemy w kolejnej lekcji).
Implementacja Dziedziczenia za Pomocą extends
Aby zadeklarować, że jedna klasa dziedziczy po innej, używamy słowa kluczowego extends
.
<?php
// Klasa bazowa (nadrzędna)
class Zwierze
{
public string $nazwa;
protected int $wiek = 0;
public function __construct(string $nazwa)
{
$this->nazwa = $nazwa;
echo "Utworzono obiekt Zwierze: " . $this->nazwa . "<br>";
}
public function jedz(string $pokarm): void
{
echo $this->nazwa . " je " . $pokarm . ".<br>";
}
public function spij(): void
{
echo $this->nazwa . " śpi.<br>";
}
public function getWiek(): int
{
return $this->wiek;
}
public function setWiek(int $wiek): void
{
if ($wiek >= 0) {
$this->wiek = $wiek;
} else {
echo "Wiek nie może być ujemny.<br>";
}
}
}
// Klasa potomna (podklasa) dziedzicząca po Zwierze
class Pies extends Zwierze
{
public string $rasa;
// Konstruktor klasy potomnej
public function __construct(string $nazwa, string $rasa)
{
// Wywołanie konstruktora klasy bazowej (Zwierze)
parent::__construct($nazwa); // `parent::` odnosi się do klasy nadrzędnej
$this->rasa = $rasa;
echo "Utworzono obiekt Pies rasy: " . $this->rasa . "<br>";
}
// Nowa metoda specyficzna dla klasy Pies
public function szczekaj(): void
{
echo $this->nazwa . " (rasa: " . $this->rasa . ") szczeka: Hau hau!<br>";
}
// Nadpisywanie metody jedz z klasy Zwierze
public function jedz(string $pokarm): void
{
if ($pokarm === "czekolada") {
echo $this->nazwa . " nie może jeść czekolady! To niezdrowe dla psów.<br>";
} else {
// Wywołanie oryginalnej metody jedz z klasy Zwierze
parent::jedz($pokarm);
echo $this->nazwa . " merda ogonem, bo dostał " . $pokarm . ".<br>";
}
}
}
$azor = new Pies("Azor", "Owczarek Niemiecki");
$azor->setWiek(5);
// Metody odziedziczone z Zwierze
$azor->jedz("karma dla psów");
$azor->jedz("czekolada"); // Wywoła nadpisaną metodę
$azor->spij();
echo $azor->nazwa . " ma " . $azor->getWiek() . " lat.<br>";
// Metoda specyficzna dla Pies
$azor->szczekaj();
echo "<hr>";
$mruczek = new Zwierze("Mruczek"); // Zwykły obiekt klasy bazowej
$mruczek->jedz("ryba");
// $mruczek->szczekaj(); // BŁĄD! Fatal error: Call to undefined method Zwierze::szczekaj()
?>
W tym przykładzie:
- Klasa
Pies
dziedziczy po klasieZwierze
. - Obiekt
$azor
(typuPies
) ma dostęp do publicznych właściwości ($nazwa
) i metod (jedz
,spij
,getWiek
,setWiek
) zdefiniowanych w klasieZwierze
. - Klasa
Pies
dodaje własną publiczną właściwość$rasa
i własną publiczną metodęszczekaj()
. - Konstruktor klasy
Pies
wywołuje konstruktor klasyZwierze
za pomocąparent::__construct($nazwa)
. Jest to bardzo ważne, aby zapewnić prawidłową inicjalizację części obiektu pochodzącej z klasy bazowej. Jeśli klasa potomna ma własny konstruktor, konstruktor klasy bazowej nie jest automatycznie wywoływany. - Klasa
Pies
nadpisuje metodęjedz()
z klasyZwierze
, dostarczając własną implementację. Wewnątrz nadpisanej metody może również wywołać oryginalną implementację z klasy bazowej za pomocąparent::jedz($pokarm)
.
Co Jest Dziedziczone?
Klasa potomna dziedziczy:
- Wszystkie publiczne i chronione (
protected
) właściwości i metody klasy bazowej. - Prywatne (
private
) właściwości i metody klasy bazowej nie są bezpośrednio dostępne w klasie potomnej (nie można się do nich odwołać za pomocą$this->prywatnaWlasciwosc
w klasie potomnej). Jednakże, jeśli publiczne lub chronione metody klasy bazowej używają jej prywatnych składowych, to te metody nadal będą działać poprawnie, gdy zostaną wywołane na obiekcie klasy potomnej. Prywatne składowe pozostają częścią obiektu, ale są dostępne tylko poprzez interfejs klasy, która je zdefiniowała.
Nadpisywanie Metod (Method Overriding)
Nadpisywanie metod to możliwość zdefiniowania w klasie potomnej metody o tej samej nazwie i takiej samej sygnaturze (lub kompatybilnej sygnaturze) co metoda w klasie bazowej. Gdy metoda jest wywoływana na obiekcie klasy potomnej, wykonana zostanie wersja metody z klasy potomnej, a nie z klasy bazowej.
Zasady nadpisywania metod:
- Nazwa metody musi być identyczna.
- Modyfikator dostępu w klasie potomnej musi być taki sam lub mniej restrykcyjny niż w klasie bazowej (np. jeśli metoda w klasie bazowej jest
protected
, w klasie potomnej może byćprotected
lubpublic
, ale nieprivate
). - Sygnatura metody (liczba i typy parametrów, typ zwracany) musi być kompatybilna. Od PHP 7.4 (z typowaniem właściwości i typami zwracanymi) obowiązują zasady kowariancji (dla typów zwracanych) i kontrawariancji (dla typów parametrów), co oznacza, że typ zwracany w metodzie nadpisującej może być bardziej szczegółowy, a typy parametrów mogą być bardziej ogólne (lub takie same). W praktyce, dla uproszczenia, często utrzymuje się identyczną sygnaturę.
- Metody zadeklarowane jako
final
w klasie bazowej nie mogą być nadpisywane (o tym za chwilę). - Metody statyczne również mogą być "nadpisywane" (a raczej redefiniowane lub ukrywane) w klasach potomnych, ale mechanizm ten działa nieco inaczej niż dla metod instancyjnych i nie podlega polimorfizmowi w ten sam sposób.
W przykładzie z klasą Pies
, metoda jedz()
została nadpisana:
<?php
// ... (definicja klasy Zwierze jak wyżej)
class Pies extends Zwierze
{
// ... (reszta klasy Pies)
// Nadpisywanie metody jedz z klasy Zwierze
public function jedz(string $pokarm): void // Ta sama nazwa i sygnatura (lub kompatybilna)
{
if ($pokarm === "czekolada") {
echo $this->nazwa . " nie może jeść czekolady! To niezdrowe dla psów.<br>";
} else {
// Wywołanie oryginalnej metody jedz z klasy Zwierze
parent::jedz($pokarm);
echo $this->nazwa . " merda ogonem, bo dostał " . $pokarm . ".<br>";
}
}
}
?>
Gdy wywołamy $azor->jedz("karma dla psów")
, wykona się kod z nadpisanej metody w klasie Pies
.
Słowo Kluczowe parent::
Słowo kluczowe parent::
pozwala na dostęp do składowych (metod i właściwości statycznych, a także konstruktora) klasy nadrzędnej z wnętrza klasy potomnej.
parent::__construct(...)
: Bardzo często używane do wywołania konstruktora klasy bazowej z konstruktora klasy potomnej.parent::nazwaMetody(...)
: Używane do wywołania oryginalnej implementacji metody z klasy bazowej, jeśli została ona nadpisana w klasie potomnej, a chcemy nadal skorzystać z funkcjonalności rodzica.parent::$wlasciwoscStatyczna
lubparent::STALA_KLASOWA
: Dostęp do statycznych właściwości lub stałych klasy nadrzędnej.
parent::
odnosi się zawsze do bezpośredniego rodzica w hierarchii dziedziczenia.
Słowo Kluczowe final
Słowo kluczowe final
może być użyte w dwóch kontekstach:
1. Metody Finalne (final function
)
Jeśli metoda w klasie bazowej jest zadeklarowana jako final
, oznacza to, że nie może być nadpisana w żadnej klasie potomnej. Próba nadpisania metody finalnej spowoduje błąd krytyczny (Fatal error).
<?php
class Urzadzenie
{
public final function getNumerSeryjny(): string
{
// Logika pobierania unikalnego numeru seryjnego, która nie powinna być zmieniana
return "SN-" . uniqid();
}
public function wlacz(): void
{
echo "Urzadzenie włączone.<br>";
}
}
class Komputer extends Urzadzenie
{
// Próba nadpisania metody finalnej - spowoduje błąd
/*
public function getNumerSeryjny(): string
{
return "KOMP-" . parent::getNumerSeryjny();
}
*/ // To by spowodowało: Fatal error: Cannot override final method Urzadzenie::getNumerSeryjny()
// Nadpisanie metody niefinalnej jest dozwolone
public function wlacz(): void
{
parent::wlacz();
echo "System operacyjny komputera startuje...<br>";
}
}
$mojKomputer = new Komputer();
echo "Numer seryjny komputera: " . $mojKomputer->getNumerSeryjny() . "<br>";
$mojKomputer->wlacz();
?>
Metody finalne są używane, gdy chcemy zagwarantować, że pewna kluczowa część implementacji klasy nie zostanie zmieniona przez klasy potomne, np. ze względów bezpieczeństwa lub integralności.
2. Klasy Finalne (final class
)
Jeśli cała klasa jest zadeklarowana jako final
, oznacza to, że nie może być ona dziedziczona. Żadna inna klasa nie może użyć jej jako klasy bazowej (nie może po niej extends
).
<?php
final class StringUtils
{
// Klasa z metodami pomocniczymi, nieprzeznaczona do dziedziczenia
public static function odwracaj(string $tekst): string
{
return strrev($tekst);
}
}
// Próba dziedziczenia po klasie finalnej - spowoduje błąd
/*
class MojeNarzędziaString extends StringUtils
{
// ...
}
*/ // To by spowodowało: Fatal error: Class MojeNarzędziaString may not inherit from final class (StringUtils)
echo StringUtils::odwracaj("Hello World") . "<br>";
?>
Klasy finalne są używane, gdy projektant klasy chce jawnie zabronić jej rozszerzania, np. dla klas narzędziowych (utility classes) lub klas, których wewnętrzna spójność mogłaby zostać naruszona przez dziedziczenie.
Konstruktory i Dziedziczenie
Jak wspomniano wcześniej, jeśli klasa potomna definiuje własny konstruktor, konstruktor klasy bazowej nie jest automatycznie wywoływany. Aby go wywołać, należy użyć parent::__construct(...)
.
Dlaczego to jest ważne? Konstruktor klasy bazowej często wykonuje kluczowe inicjalizacje (np. ustawia odziedziczone właściwości). Jeśli go nie wywołamy, obiekt może nie być w pełni lub poprawnie zainicjalizowany.
Co jeśli klasa potomna nie ma własnego konstruktora? W takim przypadku, jeśli tworzymy obiekt klasy potomnej, automatycznie zostanie wywołany konstruktor z najbliższej klasy nadrzędnej w hierarchii, która posiada konstruktor. Jeśli żadna klasa w hierarchii (aż do samej góry) nie ma konstruktora, to nic specjalnego się nie dzieje (chyba że próbujemy przekazać argumenty do new
, co może spowodować błąd, jeśli nie ma pasującego konstruktora).
<?php
class A
{
public function __construct()
{
echo "Konstruktor A<br>";
}
}
class B extends A
{
// Brak własnego konstruktora, więc wywoła się konstruktor A
}
class C extends A
{
public function __construct()
{
// parent::__construct(); // Jeśli odkomentujemy, wywoła się też konstruktor A
echo "Konstruktor C<br>";
}
}
class D extends B // B dziedziczy po A
{
public function __construct()
{
parent::__construct(); // Wywoła konstruktor B, który (ponieważ nie ma własnego) wywoła konstruktor A
echo "Konstruktor D<br>";
}
}
echo "Tworzę obiekt B:<br>";
$objB = new B(); // Wywoła konstruktor A
echo "<hr>Tworzę obiekt C:<br>";
$objC = new C(); // Wywoła tylko konstruktor C (chyba że C jawnie wywoła parent::__construct())
echo "<hr>Tworzę obiekt D:<br>";
$objD = new D(); // Wywoła konstruktor A (przez B), potem D
?>
Zawsze dobrą praktyką jest jawne wywoływanie parent::__construct()
w konstruktorze klasy potomnej, jeśli klasa bazowa ma konstruktor, który powinien być wykonany.
Podsumowanie Lekcji
W tej lekcji zgłębiliśmy mechanizm dziedziczenia w PHP. Nauczyliśmy się, jak używać słowa kluczowego extends
do tworzenia klas potomnych, które dziedziczą publiczne i chronione składowe po klasach bazowych. Zrozumieliśmy, jak nadpisywać metody, aby dostosować zachowanie w klasach potomnych, oraz jak używać parent::
do odwoływania się do implementacji z klasy nadrzędnej, w szczególności do jej konstruktora.
Omówiliśmy również słowo kluczowe final
, które pozwala na tworzenie metod finalnych (niemożliwych do nadpisania) oraz klas finalnych (niemożliwych do dziedziczenia), co daje większą kontrolę nad projektem hierarchii klas. Podkreśliliśmy znaczenie prawidłowego zarządzania konstruktorami w kontekście dziedziczenia.
Dziedziczenie jest potężnym narzędziem, które, gdy jest stosowane właściwie, prowadzi do bardziej zorganizowanego, elastycznego i reużywalnego kodu. W następnej lekcji przyjrzymy się klasom abstrakcyjnym i metodom abstrakcyjnym, które są ściśle związane z dziedziczeniem i pozwalają na definiowanie "szablonów" dla klas potomnych.
Zadanie praktyczne
Stwórz hierarchię klas dla różnych typów figur geometrycznych.
- Stwórz klasę bazową
FiguraGeometryczna
z:- Chronioną właściwością
$nazwaFigury
. - Konstruktorem przyjmującym nazwę figury.
- Publiczną metodą
getNazwa(): string
zwracającą nazwę. - Publiczną metodą
obliczPole(): float
, która na razie zwraca0.0
(będzie nadpisywana). - Publiczną metodą
obliczObwod(): float
, która na razie zwraca0.0
(będzie nadpisywana).
- Chronioną właściwością
- Stwórz klasę
Kolo
dziedziczącą poFiguraGeometryczna
:- Z prywatną właściwością
$promien
. - Konstruktorem przyjmującym promień, który wywołuje konstruktor rodzica z nazwą "Koło".
- Nadpisz metodę
obliczPole()
(pole koła: π * r^2). Możesz użyćM_PI
dla wartości π. - Nadpisz metodę
obliczObwod()
(obwód koła: 2 * π * r).
- Z prywatną właściwością
- Stwórz klasę
Prostokat
dziedziczącą poFiguraGeometryczna
:- Z prywatnymi właściwościami
$bokA
i$bokB
. - Konstruktorem przyjmującym długości boków, który wywołuje konstruktor rodzica z nazwą "Prostokąt".
- Nadpisz metodę
obliczPole()
(pole prostokąta: a * b). - Nadpisz metodę
obliczObwod()
(obwód prostokąta: 2*a + 2*b).
- Z prywatnymi właściwościami
- Przetestuj klasy: utwórz obiekty
Kolo
iProstokat
, a następnie wyświetl ich nazwy, pola i obwody.
Kliknij, aby zobaczyć przykładowe rozwiązanie
<?php
class FiguraGeometryczna
{
protected string $nazwaFigury;
public function __construct(string $nazwa)
{
$this->nazwaFigury = $nazwa;
}
public function getNazwa(): string
{
return $this->nazwaFigury;
}
public function obliczPole(): float
{
return 0.0;
}
public function obliczObwod(): float
{
return 0.0;
}
}
class Kolo extends FiguraGeometryczna
{
private float $promien;
public function __construct(float $promien)
{
parent::__construct("Koło");
$this->promien = $promien;
}
public function obliczPole(): float
{
return M_PI * pow($this->promien, 2);
}
public function obliczObwod(): float
{
return 2 * M_PI * $this->promien;
}
}
class Prostokat extends FiguraGeometryczna
{
private float $bokA;
private float $bokB;
public function __construct(float $a, float $b)
{
parent::__construct("Prostokąt");
$this->bokA = $a;
$this->bokB = $b;
}
public function obliczPole(): float
{
return $this->bokA * $this->bokB;
}
public function obliczObwod(): float
{
return 2 * $this->bokA + 2 * $this->bokB;
}
}
// Testowanie
$mojeKolo = new Kolo(5.0);
echo "Figura: " . $mojeKolo->getNazwa() . "<br>";
echo "Pole: " . round($mojeKolo->obliczPole(), 2) . "<br>";
echo "Obwód: " . round($mojeKolo->obliczObwod(), 2) . "<br>";
echo "<hr>";
$mojProstokat = new Prostokat(4.0, 7.0);
echo "Figura: " . $mojProstokat->getNazwa() . "<br>";
echo "Pole: " . round($mojProstokat->obliczPole(), 2) . "<br>";
echo "Obwód: " . round($mojProstokat->obliczObwod(), 2) . "<br>";
/* Przykładowy wynik:
Figura: Koło
Pole: 78.54
Obwód: 31.42
---
Figura: Prostokąt
Pole: 28
Obwód: 22
*/
?>
Zadanie do samodzielnego wykonania
Rozwiń hierarchię klas pojazdów.
- Stwórz klasę bazową
Pojazd
z właściwościami (np.protected string $marka
,protected string $model
,protected int $rokProdukcji
) i metodami (np.public function getInfo(): string
,public function uruchomSilnik(): void
). - Stwórz klasę
SamochodOsobowy
dziedziczącą poPojazd
, dodaj właściwość np.private int $liczbaDrzwi
i nadpisz metodęgetInfo()
, aby uwzględniała liczbę drzwi. Dodaj konstruktor. - Stwórz klasę
Motocykl
dziedziczącą poPojazd
, dodaj właściwość np.private string $typRamy
(np. "cruiser", "sportowy") i nadpisz metodęgetInfo()
. Dodaj konstruktor. - W metodzie
uruchomSilnik()
w klasiePojazd
niech wyświetla ogólny komunikat. W klasieMotocykl
nadpisz tę metodę, aby wyświetlała bardziej charakterystyczny dźwięk dla motocykla. - Przetestuj, tworząc obiekty obu klas potomnych i wywołując ich metody.
FAQ - Dziedziczenie, Nadpisywanie, final
Czy klasa potomna dziedziczy konstruktor klasy bazowej?
Jeśli klasa potomna nie definiuje własnego konstruktora, to dziedziczy i używa konstruktora klasy bazowej (jeśli istnieje). Jeśli klasa potomna definiuje własny konstruktor, konstruktor bazowy nie jest automatycznie wywoływany – trzeba go jawnie wywołać za pomocą parent::__construct()
, jeśli jest taka potrzeba.
Co się stanie, jeśli spróbuję uzyskać dostęp do prywatnej właściwości klasy bazowej z klasy potomnej?
Spowoduje to błąd (Notice: Undefined property lub Fatal error, w zależności od kontekstu i wersji PHP), ponieważ prywatne składowe są dostępne tylko wewnątrz klasy, która je zdefiniowała. Klasa potomna nie ma do nich bezpośredniego dostępu. Dostęp powinien odbywać się poprzez publiczne lub chronione metody klasy bazowej.
Czy mogę dziedziczyć po wielu klasach w PHP (wielokrotne dziedziczenie)?
Nie, PHP nie wspiera wielokrotnego dziedziczenia klas (jedna klasa może extends
tylko jedną inną klasę). Problem "diamentowy" i złożoność zarządzania hierarchią są głównymi powodami. Jednakże, od PHP 5.4 można używać Cech (Traits) do reużywania kodu i horyzontalnego współdzielenia funkcjonalności między klasami, co w pewnym stopniu adresuje potrzebę wielokrotnego dziedziczenia zachowań.
Kiedy powinienem użyć final
dla klasy lub metody?
Użyj final
dla metody, jeśli chcesz mieć pewność, że jej implementacja nie zostanie zmieniona przez żadną klasę potomną (np. dla krytycznych operacji lub algorytmów). Użyj final
dla klasy, jeśli chcesz całkowicie zabronić dziedziczenia po niej, np. dla klas narzędziowych, które nie są zaprojektowane do rozszerzania, lub gdy chcesz zachować ścisłą kontrolę nad implementacją.
Jaka jest różnica między nadpisywaniem metody a przeciążaniem metody (method overloading)?
Nadpisywanie (overriding) to definiowanie w klasie potomnej metody o tej samej sygnaturze co w klasie bazowej. Przeciążanie (overloading) to możliwość posiadania w tej samej klasie wielu metod o tej samej nazwie, ale różnych sygnaturach (np. różna liczba lub typy parametrów). PHP nie wspiera przeciążania metod w tradycyjnym sensie, jak np. Java czy C#. W PHP "przeciążanie" odnosi się raczej do metod magicznych jak __call()
i __callStatic()
, które pozwalają dynamicznie obsługiwać wywołania nieistniejących metod.
Czy mogę wywołać metodę z klasy "dziadka" (klasy nadrzędnej dla mojej klasy nadrzędnej) używając parent::parent::metoda()
?
Nie, parent::
odnosi się tylko do bezpośredniego rodzica. Aby wywołać metodę z dalszego przodka, musiałbyś znać jego nazwę i użyć NazwaDziadka::metoda()
(jeśli jest statyczna lub jeśli masz kontekst obiektu i metoda jest publiczna/chroniona i dostępna w ten sposób), lub polegać na tym, że bezpośredni rodzic poprawnie wywołuje metody swojego rodzica.
Czy właściwości mogą być final
?
Nie, słowo kluczowe final
w PHP nie może być stosowane do właściwości. Może być używane tylko dla klas i metod. Aby uczynić właściwość "niezmienną" po inicjalizacji, można użyć modyfikatora readonly
(od PHP 8.1) dla właściwości publicznych lub polegać na hermetyzacji (prywatna właściwość i brak publicznego settera).