5. Klasy
Słownictwo: Klasa (class) to typ PHP. Zmienna tego typu nazywana jest obiektem. Obiekt jest instancją (egzemplarzem) klasy.
5.1. Struktura drzewa skryptów

5.2. Każda zmienna może stać się obiektem posiadającym atrybuty
Skrypt [classes-01.php] ma następującą treść:
<?php
// obiekt generyczny
// $obj1=new stdClass();
// każda zmienna może z definicji posiadać atrybuty
$obj1->attr1 = "un";
$obj1->attr2 = 100;
// wyświetla obiekt
print "objet1=[$obj1->attr1,$obj1->attr2]\n";
// modyfikuje obiekt
$obj1->attr2 += 100;
// wyświetla obiekt
print "objet1=[$obj1->attr1,$obj1->attr2]\n";
// kopiuje wartość obiektu1 (adres obiektu, na który wskazuje) do obiektu2
// obie zmienne są wówczas różne, ale wskazują ten sam obiekt
$obj2 = $obj1;
// modyfikuje obiekt2
$obj2->attr2 = 0;
// wyświetla oba obiekty
print "objet1=[$obj1->attr1,$obj1->attr2]\n";
print "objet2=[$obj2->attr1,$obj2->attr2]\n";
// zmienia obiekt, na który wskazuje obiekt1
$obj1 = new stdClass();
print "obj1 :\n";
print_r($obj1);
print "obj2 :\n";
print_r($obj2);
// przypisuje odwołanie (adres) obiektu2 do obiektu3
// $obj2 i $obj3 są wówczas tą samą zmienną
$obj3 = &$obj2;
print "obj2 :\n";
print_r($obj2);
print "obj3 :\n";
print_r($obj3);
// modyfikuje obj3
$obj3->attr2 = 10;
// wyświetla oba obiekty
print "objet2=[$obj2->attr1,$obj2->attr2]\n";
print "objet3=[$obj3->attr1,$obj3->attr2]\n";
// zmienia obiekt, na który wskazuje obj2
$obj2 = new stdClass();
$obj2->attr3 = "deux";
$obj2->attr4 = 20;
// wyświetla oba obiekty: $obj2 i $obj3
print "obj2 :\n";
print_r($obj2);
print "obj3 :\n";
print_r($obj3);
// czy dany obiekt jest słownikiem?
print count($obj3) . "\n";
while (list($attribut, $valeur) = each($obj3)) {
print "obj3[$attribut]=$valeur\n";
}
// koniec
exit;
Wyniki:
Warning: Creating default object from empty value in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exemple_14.php on line 6
objet1=[un,100]
objet1=[un,200]
objet1=[un,0]
objet2=[un,0]
obj1 :
stdClass Object
(
)
obj2 :
stdClass Object
(
[attr1] => un
[attr2] => 0
)
obj2 :
stdClass Object
(
[attr1] => un
[attr2] => 0
)
obj3 :
stdClass Object
(
[attr1] => un
[attr2] => 0
)
objet2=[un,10]
objet3=[un,10]
obj2 :
stdClass Object
(
[attr3] => deux
[attr4] => 20
)
obj3 :
stdClass Object
(
[attr3] => deux
[attr4] => 20
)
Warning: count(): Parameter must be an array or an object that implements Countable in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exemple_14.php on line 50
1
Deprecated: The each() function is deprecated. This message will be suppressed on further calls in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exemple_14.php on line 51
obj3[attr3]=deux
obj3[attr4]=20
Uwagi
- wiersz 6: zapis $obj->attr oznacza atrybut attr zmiennej $obj. Jeśli nie istnieje, zostaje utworzony, co sprawia, że zmienna $obj staje się obiektem z atrybutami. Widzieliśmy, że PHP tworzy wówczas domyślnie obiekt typu stdClass;
- wiersz 16: wyrażenie $obj2=$obj1, gdy $obj1 jest obiektem, stanowi kopiowanie obiektów przez odwołanie: $obj2 i $obj1 są odwołaniami (adresami) do tego samego obiektu. Sam obiekt może zostać zmodyfikowany przez którekolwiek z tych odwołań;
- wiersze 23–27: mają na celu pokazanie, że $obj1 i $obj2 to dwie różne zmienne: nie znajdują się one pod tym samym adresem pamięci:
- $obj2=$obj1 skopiowało wartość $obj1 do zmiennej $obj2 (operacja 1 powyżej). Wartość $obj1 jest adresem obiektu. Zatem $obj1 i $obj2 wskazują na ten sam obiekt. Gdy operujemy na zmiennej $obj, która wskazuje na jakiś obiekt, zmienna PHP operuje na obiekcie wskazywanym przez zmienną $obj. Z poniższego schematu wynika, że obiekt, na który wskazuje zmienna, można modyfikować albo za pośrednictwem $obj1, albo za pośrednictwem $obj2. Właśnie to pokazują wiersze 4 i 5 wyników;

- wiersz 30: wyrażenie $obj3=&$obj2 powoduje, że $obj2 i $obj3 znajdują się pod tym samym adresem [1 ci-dessous]. Można by powiedzieć, że obie zmienne są aliasami tej samej lokalizacji pamięci. Obie wskazują na jeden obiekt, Obiekt A poniżej [2];
- operacja $obj2=new stdClass() powoduje utworzenie nowego obiektu, Obiektu B ([3 ci-dessous]), a adres tego nowego obiektu jest przypisywany do zmiennej $obj2. Ponieważ $obj2 i $obj3 są dwoma aliasami tej samej lokalizacji pamięci, $obj3 również wskazuje na nowy obiekt Obiekt B. Wskazują na to wiersze 16–27 i 30–41 wyników;

- wiersze 52–54: pokazują, że obiekt można przeglądać jak słownik. Kluczami słownika są nazwy atrybutów, a wartościami słownika – wartości tych samych atrybutów;
- wiersz 51: funkcja count może być zastosowana do obiektu (z ostrzeżeniem), ale nie zwraca, jak można by się spodziewać, liczby atrybutów. Obiekt wykazuje zatem podobieństwa do słownika, ale nim nie jest;
5.3. Klasa „Osoba” bez zadeklarowanych atrybutów
Skrypt [classes-02.php] wygląda następująco:
<?php
class Personne {
// atrybuty klasy
// niezadeklarowane – mogą być tworzone dynamicznie
// metoda
function identite() {
// z założenia wykorzystuje nieistniejące atrybuty
return "[$this->prenom,$this->nom,$this->age]";
}
}
// test
// atrybuty są publiczne i mogą być tworzone dynamicznie
$p = new Personne();
$p->prenom = "Paul";
$p->nom = "Langevin";
$p->age = 48;
// wywołanie metody
print "personne=" . $p->identite() . "\n";
// koniec
exit;
Wyniki:
Komentarze
- wiersze 3–13: definiują klasę Personne. Klasa to szablon, na podstawie którego tworzy się obiekty. Zawiera ona atrybuty oraz funkcje zwane metodami. Nie ma obowiązku deklarowania atrybutów;
- wiersze 8–11: metoda identité wyświetla wartości trzech atrybutów, które nie zostały zadeklarowane w klasie. Słowo kluczowe $this określa obiekt, do którego stosowana jest metoda;
- wiersz 17: tworzymy obiekt $p typu Personne. Słowo kluczowe new służy do tworzenia nowego obiektu. Operacja ta zwraca odwołanie do utworzonego obiektu (czyli adres). Możliwe są różne zapisy: new Personne(), new Personne, new personne. Nazwa klasy jest niewrażliwa na wielkość liter;
- wiersze 18–20: w obiekcie $p tworzone są trzy atrybuty niezbędne dla metody identité;
- wiersz 22: metoda identité klasy Personne jest stosowana na obiekcie $p. W kodzie (wiersze 8–11) metody identité metoda $this odwołuje się do tego samego obiektu co metoda $p;
5.4. Klasa Personne z zadeklarowanymi atrybutami
Skrypt [classes-03.php] wygląda następująco:
<?php
class Personne {
// atrybuty klasy
var $prenom;
var $nom;
var $age;
// metoda
function identite() {
return "[$this->prenom,$this->nom,$this->age]";
}
}
// test
// atrybuty są publiczne
$p = new Personne();
$p->prenom = "Paul";
$p->nom = "Langevin";
$p->age = 48;
// wywołanie metody
print "personne=" . $p->identite() . "\n";
// koniec
exit;
Wyniki:
Komentarze
- wiersze 6–8: atrybuty klasy są jawnie zadeklarowane za pomocą słowa kluczowego var;
5.5. Klasa Personne z konstruktorem
Poprzednie przykłady przedstawiały nietypowe klasy, takie jak te, które można było znaleźć w Personne 4. Nie zaleca się naśladowania tych przykładów. Przedstawiamy teraz klasę Personne [classes-04.php] zgodną z dobrymi praktykami z PHP 7:
<?php
// ścisłe przestrzeganie zadeklarowanych typów parametrów funkcji
declare (strict_types=1);
class Personne {
// atrybuty klasy
private $prenom;
private $nom;
private $age;
// metody pobierające i ustawiające
public function getPrenom(): string {
return $this->prenom;
}
public function getNom(): string {
return $this->nom;
}
public function getAge(): int {
return $this->age;
}
public function setPrenom(string $prenom): void {
$this->prenom = $prenom;
}
public function setNom(string $nom): void {
$this->nom = $nom;
}
public function setAge(int $age): void {
$this->age = $age;
}
// konstruktor
public function __construct(string $prenom, string $nom, int $age) {
// przechodzi się przez set
$this->setPrenom($prenom);
$this->setNom($nom);
$this->setAge($age);
}
// metoda toString
public function __toString(): string {
return "[$this->prenom,$this->nom,$this->age]";
}
}
// test
// utworzenie obiektu Osoba
$p = new Personne("Paul", "Langevin", 48);
// tożsamość tej osoby
print "personne=$p\n";
// zmiana wieku
$p->setAge(14);
// tożsamość osoby
print "personne=$p\n";
// koniec
exit;
Wyniki:
Komentarze
- wiersze 6–50: klasa Personne;
- wiersze 7–9: atrybuty prywatne (private) klasy. Atrybuty te są widoczne wyłącznie wewnątrz klasy. Inne dostępne słowa kluczowe to:
- public: sprawia, że atrybut jest widoczny spoza klasy,
- protected: sprawia, że atrybut protégé jest widoczny wewnątrz klasy oraz w klasach od niej pochodnych;
- ponieważ atrybuty są prywatne, nie można uzyskać do nich dostępu spoza klasy. Nie można zatem napisać następującego kodu:
W tym przypadku znajdujemy się poza klasą Personne. Ponieważ atrybut nom jest prywatny, wiersz 2 jest nieprawidłowy. Aby zainicjować pola prywatne obiektu $p, istnieją dwa sposoby:
- użycie publicznych metod set i get (nazwy tych metod mogą być dowolne) z wierszy 12–34. Można wtedy napisać:
- użyć konstruktora z wierszy 37–42. W takim przypadku należy zapisać:
Powyższy zapis automatycznie wywołuje metodę klasy Personne o nazwie __construct;
- wiersz 59: ten wiersz wyświetla osobę $p w postaci ciągu znaków. W tym celu wykorzystywana jest metoda klasy Personne o nazwie __toString (wiersze 45–47);
- wszystkie metody klasy (funkcje) zostały opatrzone prefiksem w postaci słowa kluczowego public, co oznacza, że funkcja jest widoczna poza klasą. Inne słowa kluczowe, których można używać, to – podobnie jak w przypadku atrybutów i o tym samym znaczeniu – private oraz protected. Bez jawnego atrybutu widoczności funkcja ma domyślną widoczność publiczną;
5.6. Klasa Personne z kontrolami poprawności w konstruktorze
Konstruktor klasy to odpowiednie miejsce do sprawdzenia, czy wartości inicjalizacyjne obiektu są poprawne. Jednak obiekt może być również inicjalizowany za pomocą metod set lub ich odpowiedników. Aby uniknąć umieszczania tych samych sprawdzeń w dwóch różnych miejscach, można je umieścić w metodach set. Jeśli wartość inicjalizacyjna obiektu okaże się nieprawidłowa, zostanie zgłoszony wyjątek. Oto przykład.
Najpierw przenosimy definicję klasy Personne do osobnego pliku [Personne.php]:
<?php
// ścisłe przestrzeganie zadeklarowanych typów parametrów funkcji
declare (strict_types=1);
// przestrzeń nazw;
namespace Exemples;
// klasa Osoba
class Personne {
// atrybuty klasy
private $prenom;
private $nom;
private $age;
// metody pobierające i ustawiające
public function getPrenom(): string {
return $this->prenom;
}
public function getNom(): string {
return $this->nom;
}
public function getAge(): int {
return $this->age;
}
public function setPrenom(string $prénom): void {
// imię musi być niepuste
$prénom = trim($prénom);
if ($prénom === "") {
throw new \Exception("Le prénom doit être non vide");
} else {
$this->prenom = $prénom;
}
}
public function setNom(string $nom): void {
// nazwisko nie może być puste
$nom = trim($nom);
if ($nom === "") {
throw new \Exception("Le nom doit être non vide");
} else {
$this->nom = $nom;
}
}
public function setAge(int $âge): void {
// wiek musi być prawidłowy
if ($âge < 0) {
throw new \Exception("L'âge doit être un entier positif ou nul");
} else {
$this->age = $âge;
}
}
// konstruktor
public function __construct(string $prenom, string $nom, int $age) {
// korzystamy z metod set
$this->setPrenom($prenom);
$this->setNom($nom);
$this->setAge($age);
}
// metoda
public function initWithPersonne(Personne $p): void {
// inicjuje bieżący obiekt z osobą $p
$this->__construct($p->prenom, $p->nom, $p->age);
}
// metoda toString
function __toString(): string {
return "[$this->prenom,$this->nom,$this->age]";
}
}
Komentarze
- wiersz 4: wymagamy, aby typ parametrów funkcji był zgodny z zadeklarowanym;
- wiersz 7: definiuje przestrzeń nazw (namespace). Pełna nazwa (mówimy qualifié) klasy Personne to zatem \Exemples\Personne. Należy zwrócić uwagę na znak \, który rozpoczyna nazwę kwalifikowaną: otrzymujemy wówczas nazwę kwalifikowaną absolu. Jeśli znak ten nie występuje, otrzymujemy nazwę kwalifikowaną relatif (odnoszącą się do bieżącej przestrzeni nazw). Zatem jeśli dwie klasy A i B należą do tej samej przestrzeni nazw E, w kodzie klasy A można odwołać się do klasy B za pomocą zapisu relative B. Jeśli klasa A należy do przestrzeni nazw E1, a klasa B do przestrzeni nazw E2, to w kodzie klasy A do klasy B można dotrzeć za pomocą notacji absolue \E2\B. Zdefiniowanie klasy w obrębie przestrzeni nazw nie jest obowiązkowe, ale NetBeans wyświetla ostrzeżenie, jeśli tego nie zrobimy. Dlatego to zrobimy. Ponadto przestrzenie nazw powinny odpowiadać strukturze drzewa plików. Tak więc klasa A w przestrzeni nazw E1 powinna znajdować się w pliku E1/A.php. Nie jest to obowiązkowe, ale również w tym przypadku NetBeans wyświetla ostrzeżenie, jeśli tego nie zrobimy. W przypadku klasy [\Exemples\Personne] NetBeans wyświetla ostrzeżenie, ponieważ struktura katalogowa pliku [Personne.php] to [exemples/classes/Personne.php] i w związku z tym nie odpowiada przestrzeni nazw. Nie należy mylić struktury katalogowej z przestrzenią nazw. W pełni kwalifikowana nazwa klasy wykorzystuje przestrzeń nazw i nie ma nic wspólnego ze strukturą katalogową pliku klasy PHP. Związek między strukturą katalogową a przestrzenią nazw jest opcjonalny i można go pominąć, tak jak to zrobiliśmy w tym przypadku;
- wiersze 12–14: trzy atrybuty prywatne klasy;
- wiersze 29–37: inicjalizacja atrybutu prenom i sprawdzenie wartości inicjalizacyjnej;
- wiersz 31: funkcja trim($chaine) usuwa spacje znajdujące się na początku i na końcu ciągu $chaine. Zatem trim(„abcd ”) daje ciąg „abcd”, a trim(„ ”) daje pusty ciąg;
- wiersz 32: jeśli imię jest puste, to zgłaszany jest wyjątek (wiersz 33), w przeciwnym razie imię jest zapisywane (wiersz 35). Aby zgłosić wyjątek, użyto tutaj predefiniowanej klasy [Exception]. W tym przypadku konieczne jest użycie jej nazwy bezwzględnej [\Exception]. Jeśli użyjemy jej nazwy względnej [Exception], wówczas klasa ta będzie wyszukiwana w bieżącej przestrzeni nazw, tj. w przestrzeni nazw [Exemples] klasy Personne. W ten sposób interpreter PHP będzie szukał klasy o nazwie bezwzględnej [\Exemples\Exception], która nie istnieje;
Klasa [Personne] jest wykorzystywana przez następujący skrypt [classes-05.php]:
<?php
// ścisłe przestrzeganie typów parametrów funkcji
declare(strict_types=1);
// włączenie definicji klasy Osoba
require_once __DIR__."/Personne.php";
// kwalifikowana nazwa klasy „Osoba”
use \Exemples\Personne;
// test
// utworzenie obiektu klasy „Osoba”
$p = new Personne("Paul", "Langevin", 48);
// tożsamość tej osoby
print "Exemple1, personne=$p\n";
// utworzenie błędnego obiektu klasy „Osoba”
try {
$p = new Personne("xx", "yy", "zz");
} catch (\Exception $e1) {
print "Exemple2, erreur : " . $e1->getMessage() . "\n";
} catch (\TypeError $e2) {
print "Exemple2, erreur : " . $e2->getMessage() . "\n";
}
// błędne utworzenie obiektu „Osoba”
try {
$p = new Personne("", "yy", 10);
} catch (\Exception $e1) {
print "Exemple3, erreur : " . $e1->getMessage() . "\n";
} catch (\TypeError $e2) {
print "Exemple3, erreur : " . $e2->getMessage() . "\n";
}
// koniec
exit;
Komentarze
- wiersz 7: skrypt będzie korzystał z klasy [Personne]. Należy zatem wskazać interpreterowi PHP, gdzie może znaleźć definicję tej klasy. Tą rolę pełnią instrukcje [include fichier] i [require fichier]. W tym przypadku użyto instrukcji [include]. Różnica między tymi dwiema instrukcjami jest następująca: jeśli instrukcja [include fichier] napotka błędy podczas ładowania [fichier], generowany jest błąd na poziomie [E_WARNING], alewykonanie jest kontynuowane, podczas gdy [require] w tej samej sytuacji generuje błąd krytyczny i wykonywanie skryptu zostaje zatrzymane. Każda z tych dwóch instrukcji ma swoją odmianę: [include_once] i [require_once]. Te dwie odmiany pozwalają obsłużyć przypadek wielokrotnego dołączania tego samego pliku. Można sobie tutaj wyobrazić projekt składający się z kilku skryptów PHP, z których kilka odwołuje się do klasy [Personne]. Ich wykonanie spowoduje wówczas wielokrotne dołączenie pliku [Personne.php] i wygeneruje błąd, ponieważ klasy nie można zdefiniować dwukrotnie. Rozwiązaniem jest użycie wariantów [_once], które gwarantują, że plik zostanie dołączony tylko raz w globalnym skrypcie projektu;
- wiersz 7: stała [__DIR__] jest stałą PHP, która określa pełną nazwę folderu, w którym znajduje się skrypt zawierający stałą [__DIR__]. Zatem wyrażenie w wierszu 17:
require_once __DIR__."/Personne.php";
będzie równoważne mniej więcej temu:
require_once ‘C:\Data\st-2019\dev\php7\php5-exemples\exemples\classes/Personnes.php’
W ścieżce do pliku można stosować zarówno znaki /, jak i \;
- wiersz 14: używana jest klasa [Personne], którą właśnie zdefiniowaliśmy. Skrypt [classes-05.php] nie posiada przestrzeni nazw. W wierszu 14 używana jest względna nazwa klasy [Personne] bez przestrzeni nazw. Wobec braku przestrzeni nazw klasy [Personne] jest ona wyszukiwana w samym skrypcie [classes-05.php] i w związku z tym nie zostanie znaleziona. Istnieją dwa rozwiązania tego problemu:
- użycie pełnej nazwy klasy [\Exemples\Personne];
- użycie instrukcji use w wierszu 10. Instrukcja ta wskazuje, że następujący po niej kod korzysta z klasy [\Exemples\Personne];
- wiersz 10: instrukcja „use” pozwala interpreterowi rozpoznać, że klasa [Personne], do której odwołuje się wiersz 14, to w rzeczywistości klasa [\Exemples\Personne]. Skąd jednak interpreter ma znaleźć kod tej klasy? Informację tę zawiera wiersz 7. Wskazuje on, że do wykonania bieżącego skryptu należy również załadować skrypt [Personne.php]. Użyto tu względnej nazwy pliku. Będzie on zatem wyszukiwany w folderze zawierającym skrypt [classes-05.php]. Skrypty [Personne.php] i [classes-05.php] muszą zatem znajdować się w tym samym folderze. Tak jest w tym przypadku, gdzie oba znajdują się w folderze [exemples/classes]. Instrukcja w wierszu 10 jest równoważna z:
use \Exemples\Personne as Personne;
Powyższa instrukcja [use] oznacza, że alias [Personne] odnosi się do klasy [\Exemples\Personne];
- wiersz 14: tworzony jest obiekt [Personne]. W tym miejscu zostanie niejawnie wykonana metoda [__construct] klasy [Personne];
- wiersz 16: powoduje wyświetlenie osoby $p. Aby wartość zmiennej $p mogła zostać wyświetlona, musi zostać przekształcona w ciąg znaków. Domyślnie wykonywana jest wówczas metoda [Personne.__toString]. Musi ona zatem zwrócić ciąg znaków;
- widzieliśmy, że konstruktor klasy [Personne] może wygenerować wyjątek typu [\Exception]. Należy więc obsłużyć ten wyjątek. Kod w wierszu 14 jest zatem niekompletny. Aby poprawnie obsłużyć wyjątek, który może wystąpić, należy wykorzystać kod z wierszy 18–24. W tym przypadku celowo generujemy wyjątek, przekazując wiek, który nie jest liczbą całkowitą. W tym konkretnym przypadku wyjątek jest generowany przez interpreter PHP, a nie przez kod klasy [Personne]. Sygnatura metody [Personne.__construct] wygląda bowiem następująco:
function __construct(string $prenom, string $nom, int $age)
Parametr [age] przekazywany do konstruktora musi więc być typu całkowitego. Jeśli tak nie jest, interpreter PHP zgłasza błąd typu [TypeError]. Z kolei metody [set] klasy [Personne] generują wyjątek typu [\Exception]. Ponieważ konstruktor, który je wywołuje, nie posiada struktury try/catch, wyjątek jest przekazywany o jeden poziom wyżej, do kodu, który wywołał konstruktor, tj. do kodu skryptu [classes-05.php]. Ostatecznie skrypt [classes-05.php] może otrzymać dwa rodzaje wyjątków: \Exception lub \TypeError. Należy zauważyć, że gdy programista ma pewność, iż pewne wyjątki nie mogą wystąpić, nie będzie stosował odpowiednich opcji catch. W tym przypadku są one stosowane systematycznie wyłącznie w celach demonstracyjnych. Opcje catch będą jednak stosowane dla każdego możliwego wyjątku, nawet mało prawdopodobnego;
Z tego powodu struktura try w wierszach 18–24 zawiera dwa bloki catch, aby oddzielnie obsłużyć oba typy wyjątków;
- wiersz 20: można zapisać zarówno [Exception], jak i [\Exception]:
- pierwsza wersja wykorzystuje względną nazwę klasy, odnoszącą się do przestrzeni nazw skryptu. Skrypt ten nie posiada przestrzeni nazw. Jego przestrzenią nazw jest zatem katalog główny przestrzeni nazw: \. Zatem wpisanie tutaj [Exception] jest równoznaczne z wpisaniem [\Exception]. Tymczasem klasa [Exception] rzeczywiście znajduje się w przestrzeni nazw [\];
Zaleca się stosowanie absolutnej nazwy predefiniowanych wyjątków z klasy PHP w skrypcie, który sam nie posiada przestrzeni nazw. Jeśli więc zdecydujemy się nadać temu skryptowi przestrzeń nazw, zapis absolutnych nazw klas pozostaje poprawny, podczas gdy w przeciwnym przypadku zmiana przestrzeni nazw spowoduje błędy w nazwach względnych klas;
- wiersz 21: w przypadku wystąpienia wyjątku metoda [Exception→getMessage] pozwala uzyskać komunikat o błędzie związany z tym wyjątkiem. To samo dotyczy błędu typu [TypeError]. W metodzie [Personne.setPrenom] zapisano:
public function setPrenom(string $prénom) {
// imię nie może być puste
$prénom = trim($prénom);
if ($prénom === "") {
throw new \Exception("Le prénom doit être non vide");
} else {
$this->prenom = $prénom;
}
}
W wierszu 5 zgłaszany jest wyjątek z komunikatem o błędzie [Le prénom doit être non vide]. Komunikat ten zostanie pobrany przez metodę [Exception→getMessage] w wierszu 29 skryptu [classes-05.php].
Wyniki:
Exemple1, personne=[Paul,Langevin,48]
Exemple2, erreur : Argument 3 passed to Exemples\Personne::__construct() must be of the type integer, string given, called in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exemple_18.php on line 19
Exemple3, erreur : Le prénom doit être non vide
5.7. Dodanie metody pełniącej rolę drugiego konstruktora
W PHP 7 nie ma możliwości posiadania wielu konstruktorów z różnymi parametrami, które pozwalałyby na tworzenie obiektu na różne sposoby. Można zatem używać metod pełniących rolę konstruktora:
// metoda
public function initWithPersonne(Personne $p) {
// inicjuje bieżący obiekt z osobą $p
$this->__construct($p->prenom, $p->nom, $p->age);
}
Komentarze
- wiersze 2–5: metoda initWithPersonne pozwala przypisać do bieżącego obiektu wartości atrybutów innego obiektu Personne. W tym przypadku wywołuje ona konstruktor __construct, ale nie jest to obowiązkowe. Mogłaby sama zainicjować atrybuty klasy [Personne];
Nowa klasa [Personne] jest wykorzystywana przez następujący skrypt [classes-06.php]:
<?php
// włączenie definicji klasy Osoba
require_once __DIR__."/Personne.php";
// deklaracja kwalifikowanej nazwy klasy Osoba
use \Exemples\Personne;
// test
// utworzenie obiektu Osoba
try {
$p = new Personne("Paul", "Langevin", 48);
} catch (\Exception $e) {
print "erreur : " . $e->getMessage();
exit;
}
// tożsamość tej osoby
print "personne=$p\n";
// utworzenie drugiej osoby
try {
$p2 = new Personne("Laure", "Adeline", 67);
} catch (\Exception $e) {
print "erreur : " . $e->getMessage();
exit;
}
// inicjalizacja pierwszej osoby wartościami drugiej
try {
$p->initWithPersonne($p2);
} catch (\Exception $e) {
print "erreur : " . $e->getMessage();
exit;
}
// weryfikacja
print "personne=$p\n";
// koniec
exit;
- wiersze 14, 23, 30: często zdarza się, że po wystąpieniu wyjątku konieczne jest zatrzymanie wykonywania skryptu konsolowego, jeśli napotkany błąd jest nieodwracalny. W skrypcie internetowym sytuacja wygląda inaczej: nie przerywa się wykonywania skryptu, lecz wyświetla się stronę błędu. Jeśli znajdujemy się w funkcji, nie stosuje się instrukcji exit, lecz return: nie przerywa się wykonywania skryptu (exit), lecz wychodzi się z funkcji (return) po zgłoszeniu błędu;
Wyniki:
5.8. Tabela obiektów [Personne]
Poniższy przykład [classes-07.php] pokazuje, że można tworzyć tablice obiektów.
<?php
require_once __DIR__."/Personne.php";
use \Exemples\Personne;
// test
// utworzenie tablicy obiektów Personne
// aby ułatwić zrozumienie kodu, nie obsługujemy ewentualnego wyjątku
$groupe = [new Personne("Paul", "Langevin", 48), new Personne("Sylvie", "Lefur", 70)];
// tożsamość tych osób
for ($i = 0; $i < count($groupe); $i++) {
print "groupe[$i]=$groupe[$i]\n";
}
// koniec
exit;
Wyniki:
Komentarze
- wiersz 9: utworzenie tablicy zawierającej 2 osoby;
- wiersz 12: przeglądanie tablicy;
- wiersz 13: $groupe[$i] jest obiektem typu Personne. Do jego wyświetlenia używana jest metoda [Personne.__toString];
5.9. Tworzenie klasy pochodnej od klasy Personne
W pliku [Enseignant.php] tworzymy następującą klasę [Enseignant]:
<?php
// ścisłe przestrzeganie zadeklarowanych typów parametrów funkcji
declare (strict_types=1);
// przestrzeń nazw
namespace Exemples;
// klasa pochodna klasy „osoba”
class Enseignant extends Personne {
// atrybuty
private $discipline; // nauczana dyscyplina
// metody getter i setter
public function getDiscipline(): string {
return $this->discipline;
}
public function setDiscipline(string $discipline): void {
$this->discipline = $discipline;
}
// konstruktor
public function __construct(string $prénom, string $nom, int $âge, string $discipline) {
// atrybuty nadrzędne
parent::__construct($prénom, $nom, $âge);
// inne atrybuty
$this->setDiscipline($discipline);
}
// przeładowanie funkcji __toString klasy nadrzędnej
public function __toString(): string {
return "[" . parent::__toString() . ",$this->discipline]";
}
}
Komentarze
- wiersz 7: klasa [Enseignant] również należy do przestrzeni nazw [Exemples];
- wiersz 10: klasa Enseignant wywodzi się (extends) z klasy Personne.. Klasa pochodna Enseignant dziedziczy atrybuty i metody po swojej klasie nadrzędnej;
- wiersz 12: klasa Enseignant dodaje nowy, własny atrybut discipline;
- wiersz 25: konstruktor klasy Enseignant przyjmuje 4 parametry:
- 3 do zainicjowania klasy nadrzędnej (imię, nazwisko, wiek), wiersz 27;
- 1 do własnej inicjalizacji (przedmiot), wiersz 29;
- wiersz 27: klasa pochodna ma dostęp do metod i konstruktorów swojej klasy nadrzędnej za pomocą słowa kluczowego parent ::. W tym miejscu przekazujemy parametry (imię, nazwisko, wiek) do konstruktora klasy nadrzędnej;
- wiersze 33–35: metoda __toString klasy pochodnej wykorzystuje metodę __toString klasy nadrzędnej;
Klasa [Enseignant] jest wykorzystywana przez następujący skrypt [classes-08.php]:
<?php
// włączenie definicji obu klas
require_once __DIR__."/Personne.php";
require_once __DIR__."/Enseignant.php";
// deklaracja obu używanych klas
use \Exemples\Personne;
use \Exemples\Enseignant;
// test
// utworzenie tablicy obiektów klasy Personne i klas pochodnych
// dla uproszczenia przykładu nie obsługujemy wyjątków
$groupe = array(new Enseignant("Paul", "Langevin", 48, "anglais"), new Personne("Sylvie", "Lefur", 70));
// tożsamość tych osób
for ($i = 0; $i < count($groupe); $i++) {
print "groupe[$i]=$groupe[$i]\n";
}
// koniec
exit;
Komentarze
- wiersze 4–5: musimy wskazać interpreterowi PHP, gdzie znajdują się dwie klasy [Enseignant, Personne];
- wiersze 7–8: deklaracja pełnych nazw obu klas. Pozwoli nam to na odwoływanie się do nich w kodzie po prostu poprzez ich nazwy, bez przyrostka przestrzeni nazw;
- wiersz 13: tworzymy tablicę zawierającą typ [Personne] i typ [Enseignant];
- wiersze 16–18: wyświetlają elementy tablicy;
- wiersz 17: zostanie wywołana metoda __toString każdego elementu $groupe[$i]. Klasa Personne posiada metodę __toString. Klasa Enseignant posiada dwie metody: metodę swojej klasy nadrzędnej oraz własną. Można się zastanawiać, która z nich zostanie wywołana. Wykonanie kodu pokazuje, że wywołana została metoda klasy Enseignant. Zawsze tak jest: gdy wywoływana jest metoda na obiekcie, jest ona wyszukiwana w następującej kolejności: w samym obiekcie, w jego klasie nadrzędnej, jeśli taka istnieje, następnie w klasie nadrzędnej klasy nadrzędnej itd. Wyszukiwanie kończy się, gdy tylko metoda zostanie znaleziona.
Wyniki:
5.10. Utworzenie drugiej klasy pochodnej od klasy Personne
Poniższy przykład tworzy klasę Etudiant, wywodzącą się z klasy Personne, w pliku [Etudiant.php]:
<?php
// ścisłe przestrzeganie zadeklarowanych typów parametrów funkcji
declare (strict_types=1);
// przestrzeń nazw
namespace Exemples;
class Etudiant extends Personne {
// atrybuty
private $formation; // ukończone szkolenia
// metody getter i setter
public function getFormation(): string {
return $this->formation;
}
public function setFormation(string $formation): void {
$this->formation = $formation;
}
// konstruktor
public function __construct(string $prénom, string $nom, int $âge, string $formation) {
// atrybuty nadrzędne
parent::__construct($prénom, $nom, $âge);
// inne atrybuty
$this->setFormation($formation);
}
// przeładowanie funkcji __toString klasy nadrzędnej
public function __toString(): string {
return "[" . parent::__toString() . ",$this->formation]]";
}
}
Klasa ta jest wykorzystywana przez następujący skrypt [classes-09.php]:
<?php
// dołączenie i definicja klas wykorzystywanych przez skrypt
require_once __DIR__."/Personne.php";
use \Exemples\Personne;
require_once __DIR__."/Enseignant.php";
use \Exemples\Enseignant;
require_once __DIR__."/Etudiant.php";
use \Exemples\Etudiant;
// test
// utworzenie tablicy obiektów typu „osoba” i pochodnych
// aby ułatwić zrozumienie przykładu, nie obsługujemy wyjątków
$groupe = array(new Enseignant("Paul", "Langevin", 48, "anglais"), new Personne("Sylvie", "Lefur", 70), new Etudiant("Steve", "Boer", 23, "iup2 qualité"));
// tożsamość tych osób
for ($i = 0; $i < count($groupe); $i++) {
print "groupe[$i]=$groupe[$i]\n";
}
// koniec
exit;
Wyniki:
groupe[0]=[[Paul,Langevin,48],anglais]
groupe[1]=[Sylvie,Lefur,70]
groupe[2]=[[Steve,Boer,23],iup2 qualité]
5.11. Związek konstruktora klasy pochodnej z konstruktorem klasy nadrzędnej
W niektórych językach obiektowych konstruktor klasy pochodnej automatycznie wywołuje konstruktor klasy nadrzędnej. Poniższy kod [classes-16.php] pokazuje, że w przypadku PHP 7 tak nie jest:
<?php
class Classe1 {
// konstruktor
public function __construct() {
print "constructeur de la classe Classe1\n";
}
}
class Classe2 extends Classe1 {
// konstruktor
public function __construct() {
// konstruktor klasy nadrzędnej nie jest wywoływany domyślnie
print "constructeur de la classe Classe2\n";
}
}
class Classe3 extends Classe1 {
// konstruktor
public function __construct() {
// jawne wywołanie konstruktora klasy nadrzędnej
parent::__construct();
// kod właściwy dla klasy Classe3
print "constructeur de la classe Classe3\n";
}
}
// testy
print "test1---------\n";
new Classe2();
print "test2---------\n";
new Classe3();
Wyniki
5.12. Przedefiniowanie metody klasy nadrzędnej
Widzieliśmy już, że metodę klasy nadrzędnej można przedefiniować w klasie podrzędnej. W ten sposób metoda [__toString] klasy [Personne] (patrz link) została zdefiniowana na nowo w klasach potomnych [Enseignant] (patrz link) oraz [Etudiant] (patrz link). Skrypt [classes-13.php] ponownie ilustruje tę koncepcję:
<?php
// ścisłe przestrzeganie typów parametrów funkcji
declare(strict_types=1);
// klasa główna
class Classe1 {
public function f(): int {
return 1;
}
function g(): int {
return 2;
}
}
// klasa pochodna
class Classe2 extends Classe1 {
// prze definiowanie funkcji f klasy nadrzędnej
public function f(): int {
return parent::f() + 10;
}
}
// kod
$c2 = new Classe2();
print $c2->f() . "\n";
print $c2->g() . "\n";
$c1=new Classe1();
print $c1->f()."\n";
Komentarze
- wiersze 7–17: klasa [Classe1] definiuje dwie metody f i g;
- wiersze 20–27: klasa [Classe2] rozszerza klasę [Classe1] i redefiniuje jej metodę f;
Wyniki
Komentarze
- w wierszu 30 kodu tworzony jest obiekt $c2 typu [Classe2];
- w linii 31 kodu wywoływana jest metoda f obiektu $c2. Ponieważ metoda ta istnieje, zostaje wykonana;
- w wierszu 32 kodu wywoływana jest metoda g obiektu $c2. Ponieważ metoda ta nie istnieje, jest ona wyszukiwana w klasie nadrzędnej, gdzie zostaje znaleziona i wykonana;
- w linii 33 kodu tworzony jest obiekt $c1 typu [Classe1];
- w linii 34 kodu wywoływana jest metoda f obiektu $c1. Ponieważ metoda ta istnieje, zostaje wykonana;
5.13. Przekazywanie obiektu jako parametru funkcji
Rozważmy następujący skrypt [classes-14.php]:
<?php
// ścisłe przestrzeganie typów parametrów funkcji
declare(strict_types=1);
// klasa główna
class Classe1 {
public function f(): int {
return 1;
}
function g(): int {
return 2;
}
}
// klasa pochodna
class Classe2 extends Classe1 {
// prze definiuje się funkcję f klasy nadrzędnej
public function f(): int {
return parent::f() + 10;
}
}
// parametr funkcji jest typu Klasa1 lub typu pochodnego
function doSomething(Classe1 $c1): void {
print $c1->f() + $c1->g() . "\n";
}
// kod
// tworzy się obiekt typu Klasa2 wywodzący się z Klasy1
$c2 = new Classe2();
// wywołuje się funkcję doSomething z
doSomething($c2);
Komentarze
- wiersze 7–17: klasa [Classe1];
- wiersze 20–27: klasa [Classe2] wywodząca się z klasy [Classe1];
- wiersz 30: funkcja oczekująca parametru typu [Classe1]. Gdy oczekiwanym typem jest klasa, rzeczywisty parametr może być obiektem typu oczekiwanego lub pochodnego;
- wiersze 35–38: wywołano funkcję [doSomething] z parametrem typu [Classe2], podczas gdy oczekiwany jest typ [Classe1];
Wyniki
5.14. Klasy abstrakcyjne
Klasa abstrakcyjna to niekompletna klasa, której nie można instancjonować. Aby można było z niej korzystać, musi być ona obowiązkowo odziedziczona.
Do czego służy klasa abstrakcyjna? Czasami mamy do czynienia z klasami, które mają wspólną metodę lub metody, ale różnią się innymi metodami lub atrybutami. W takim przypadku wskazane jest zebranie wszystkich wspólnych elementów w klasie nadrzędnej. Na razie nie potrzebujemy klasy abstrakcyjnej. Załóżmy jednak, że klasy potomne różnią się jedynie metodą M: sygnatura tej metody byłaby taka sama we wszystkich klasach potomnych, ale jej implementacja byłaby różna. Aby wymusić na klasach potomnych implementację metody M:
- zadeklarujemy sygnaturę metody M w klasie nadrzędnej. Ponieważ klasa ta nie wie, jak ją zaimplementować, poprzedzamy nazwę metody słowem kluczowym abstract: oznacza to, że implementacja metody M zostaje przeniesiona na klasy potomne;
- ponieważ klasa nadrzędna nie jest w pełni zaimplementowana, jest ona również zadeklarowana jako abstrakcyjna za pomocą tego samego słowa kluczowego „abstract”. Powoduje to, że klasa ta nie może być już instancjonowana. Konieczne jest utworzenie klasy potomnej, która zdefiniuje implementację metody M, aby treść klasy nadrzędnej mogła być wykorzystana;
Oto przykład [classes-15.php]:
<?php
// ścisłym przestrzeganiem typów parametrów funkcji
declare(strict_types=1);
// główna klasa abstrakcyjna
abstract Class Classe1 {
// metoda znana wszystkim klasom pochodnym
public function f(): int {
return 1;
}
// abstrakcyjna metoda g – zostanie zdefiniowana przez klasy pochodne
abstract function g(): int;
}
// klasa pochodna
Class Classe2 extends Classe1 {
// metoda g klasy nadrzędnej musi zostać zdefiniowana
public function g(): int {
return parent::f() + 10;
}
}
// klasa pochodna
Class Classe3 extends Classe1 {
// metoda g klasy nadrzędnej musi być zdefiniowana
public function g(): int {
return parent::f() + 20;
}
// można przedefiniować metodę f klasy nadrzędnej
public function f(): int {
return 2;
}
}
// kod
$c2 = new Classe2();
print $c2->f() . "\n";
print $c2->g() . "\n";
$c3 = new Classe3();
print $c3->f() . "\n";
print $c3->g() . "\n";
Komentarze
- wiersze 7–16: klasa [Classe1] jest abstrakcyjna (wiersz 7), ponieważ nie implementuje metody g z wiersza 15. Aby można było z niej korzystać, musi więc zostać odziedziczona;
- wiersze 19–26: klasa [Classe2] rozszerza klasę [Classe1] i redefiniuje metodę g swojej klasy nadrzędnej (wiersze 22–24);
- wiersze 29–41: klasa [Classe3] rozszerza klasę [Classe1] i redefiniuje metodę g swojej klasy nadrzędnej (wiersze 32–34);
- wiersze 37–39: klasa [Classe3] redefiniuje metodę f swojej klasy nadrzędnej;
- wiersze 44–49: tworzone są dwa obiekty typu [Classe2] i [Classe3] oraz wywoływane są ich metody f i g;
Wyniki
5.15. Klasy końcowe
Klasa końcowa to klasa, od której nie można tworzyć klas pochodnych. Rozważmy następujący skrypt [classes-11.php]:
<?php
// przestrzeń nazw
namespace Exemples;
// klasa niepochodna
final Class Classe1 {
}
// klasa pochodna
Class Classe2 extends Classe1 {
}
// kod – musi powodować błąd
new Classe2();
Komentarze
- wiersze 7–9: słowo kluczowe „final” sprawia, że klasa [Classe1] staje się klasą końcową, od której nie można tworzyć klas pochodnych;
- wiersze 12–14: klasa [Classe2] dziedziczy po klasie finalnej [Classe1], co stanowi błąd;
- wiersz 17: błąd zostanie zgłoszony dopiero podczas wykonywania skryptu, gdy spróbujemy operować na obiekcie typu [Classe2];
Wyniki
5.16. Metody końcowe
Metoda końcowa to metoda, której nie można przedefiniować poprzez dziedziczenie. Oto przykład [classes-12.php]:
<?php
// ścisłe przestrzeganie typów parametrów funkcji
declare(strict_types=1);
// przestrzeń nazw
namespace Exemples;
// klasa główna
Class Classe1 {
// tej metody nie można przedefiniować w klasie pochodnej
public final function f(): int {
return 1;
}
}
// klasa pochodna
Class Classe2 extends Classe1 {
public function f(): int {
return 2;
}
}
// kod – musi powodować błąd
new Classe2();
Komentarze
- wiersz 13: metoda f klasy [Classe1] została zadeklarowana jako finalna za pomocą słowa kluczowego final;
- wiersz 20: klasa [Classe2] dziedziczy po klasie [Classe1];
- wiersze 22–23: funkcja f klasy nadrzędnej [Classe1] jest redefiniowana. Powinno to spowodować błąd;
- wiersz 29: tworzy się obiekt typu [Classe2], aby zmusić interpreter PHP do sprawdzenia klasy [Classe2];
Wyniki
5.17. Metody i atrybuty statyczne
Metoda statyczna to metoda powiązana z klasą, w której jest zdefiniowana, a nie z obiektami będącymi instancjami tej klasy. Jeśli więc klasa C deklaruje metodę statyczną M, to aby z niej skorzystać, należy napisać:
- C::M, jeśli znajdujemy się poza klasą;
- self::M, jeśli znajdujemy się wewnątrz klasy;
Oto przykład [classes-17.php]:
<?php
class Classe1 {
// metoda statyczna
static function say(string $message): void {
print "$message\n";
}
}
// test -------------------
Classe1::say("hello");
Komentarze
- wiersz 6: metoda [say] jest zadeklarowana jako statyczna za pomocą słowa kluczowego static;
- wiersz 13: wywołanie metody statycznej [say] w notacji: Classe1::say;
Wyniki
Rozważmy teraz następujący kod [classes-18.php]:
<?php
class Classe1 {
// atrybut statyczny
private static $nbObjects = 0;
public function __construct() {
print "constructeur Classe1\n";
self::$nbObjects++;
}
// metoda statyczna
static function say(): void {
print self::$nbObjects ." objets de type [Classe1] ont été construits\n";
}
}
// test -------------------
new Classe1();
new Classe1();
Classe1::say();
Komentarze
- wiersz 5: deklarowany jest atrybut statyczny, który będzie zliczał liczbę utworzonych instancji klasy [Classe1]. Nie jest to atrybut, który może należeć do instancji tej klasy. W rzeczywistości, jeśli utworzone zostaną dwa obiekty O1 i O2, żaden z nich nie ma wiedzy o istnieniu drugiego. Posiadanie licznika w instancji nie ma sensu: kiedy tworzony jest nowy obiekt, w której instancji należy zwiększyć licznik? Byłoby konieczne zwiększenie licznika konkretnego obiektu, pomijając liczniki pozostałych instancji. Atrybut statyczny jest atrybutem klasy, a nie instancji tej klasy;
- wiersze 7–10: to właśnie w konstruktorze będziemy zliczać utworzone obiekty, ponieważ utworzenie każdego nowego obiektu powoduje wykonanie konstruktora;
- wiersz 14: należy zwrócić uwagę na zapis self::$nbObjects, który wskazuje, że odwołujemy się do atrybutu statycznego klasy, w której znajduje się wykonywany kod;
- wiersze 13–15: statyczna metoda [say] służy do wyświetlania liczby utworzonych obiektów;
- wiersze 20–22: tworzymy dwa obiekty i wyświetlamy licznik obiektów;
Wyniki
constructeur Classe1
constructeur Classe1
2 objets de type [Classe1] ont été construits
5.18. Widoczność między klasą nadrzędną a klasą podrzędną
Przyjrzyjmy się poniższemu skryptowi [classes-19.php]:
<?php
class SomeParent {
// atrybut
private $attributeOfParent = 4;
// metoda
public function doTest(): void {
// kto wywołuje?
print "parent :\n";
var_dump($this);
// wyświetlanie nadrzędne
print "parent : attributeOfParent={$this->attributeOfParent}\n";
print "parent : attributeOfChild={$this->attributeOfChild}\n";
}
}
class SomeChild extends SomeParent {
// atrybut
private $attributeOfChild = 14;
// metoda
public function doTest(): void {
// widok potomny
print "child : attributeOfParent={$this->attributeOfParent}\n";
print "child : attributeOfChild={$this->attributeOfChild}\n";
// element nadrzędny
parent::doTest();
}
}
// skrypt główny
print "---test1\n";
(new SomeParent())->doTest();
print "---test2\n";
(new SomeChild())->doTest();
Komentarze
- wiersze 3–17: klasa [SomeParent];
- wiersze 19–32: klasa potomna [SomeChild]. Widać, że rozszerza ona klasę [SomeParent] (wiersz 19);
- wiersz 5: klasa [SomeParent] ma tylko jeden atrybut;
- wiersze 8–17: metoda [SomeParent::doTest] służy do wyświetlania dwóch atrybutów:
- [$attributeOfParent], należący do klasy [SomeParent];
- [$attributeOfChild], należący do klasy [SomeChild] (wiersz 21);
- wiersze 10–11: wyświetlana jest tożsamość dzwoniącego: metoda ta będzie wywoływana na dwa różne sposoby:
- z klasy nadrzędnej [SomeParent];
- z klasy potomnej [SomeChild];
- wiersze 13–14: wyświetlenie obu atrybutów;
- wiersze 19–32: klasa potomna [SomeChild], która rozszerza klasę [SomeParent] (wiersz 19);
- wiersz 21: klasa [SomeChild] ma tylko jeden atrybut;
- wiersz 24: metoda [SomeChild::doTest] służy do wyświetlenia dwóch atrybutów:
- [$attributeOfParent], należący do klasy [SomeParent];
- [$attributeOfChild], należący do klasy [SomeChild];
- wiersze 26–27: wyświetlenie obu atrybutów;
- wiersz 30: wywołanie metody [doTest] klasy nadrzędnej, która z kolei wyświetli oba atrybuty;
- wiersz 36: wywoływana jest metoda [SomeParent::doTest];
- wiersz 38: wywołana zostaje metoda [SomeChild::doTest];
W pierwszym teście widoczność obu atrybutów wynosi [private]. Można zatem oczekiwać, że klasa potomna nie widzi atrybutu swojej klasy nadrzędnej. Atrybut ten musiałby mieć co najmniej widoczność [protected]. A co z atrybutem klasy potomnej? Czy jest on widoczny w klasie nadrzędnej?
Oto wyniki tego pierwszego testu:
Komentarze
- wiersze 1–10: wyniki pierwszego testu, w którym wywoływana jest metoda [SomeParent::doTest];
- wiersze 3–6: widać, że obiekt wywołujący metodę jest typu [SomeParent];
- wiersz 7: wyświetlenie atrybutu [$attributeOfParent];
- wiersze 9–10: widać, że atrybut [SomeParent::$attributeOfChild] nie istnieje. Nie jest więc wyświetlany;
- wiersze 11–30: wyniki drugiego testu, w którym wywoływana jest metoda [SomeChild::doTest];
- wiersze 13–14: widać, że atrybut [SomeChild::$attributeOfParent] nie istnieje. Nie jest on zatem wyświetlany. Jest to normalne: atrybut [SomeParent::$attributeOfParent] to [private], a zatem nie jest znany w klasie potomnej;
- wiersz 15: wyświetlenie atrybutu [$attributeOfChild];
- wiersze 16–30: znajdujemy się w metodzie [SomeParent::doTest] wywołanej przez klasę potomną;
- wiersze 17–22: widać, że [$this] jest typu [SomeChild] i posiada dwa atrybuty prywatne;
- wiersz 23: co jest bardzo zaskakujące, klasa [$this] typu [SomeChild] widzi tutaj atrybut klasy nadrzędnej [$attributeOfParent];
- wiersze 25–30: równie zaskakujące jest to, że element [$this] typu [SomeChild] nie widzi swojego atrybutu [$attributeOfChild];
Ten wynik jest bardzo zaskakujący: mimo że wiersze 17–21 wskazują, że [$this] jest typu [SomeChild], to ten obiekt [$this] wewnątrz metody [SomeParent::doTest] zachowuje się tak, jakby był instancją klasy [SomeParent], a nie klasy [SomeChild].
Przeprowadźmy nowy test ze skryptem [classes-20.php]. Atrybut [$attributeOfParent] ma teraz widoczność [protected] (wiersz 5):
<?php
class SomeParent {
// atrybut
protected $attributeOfParent = 4;
// metoda
public function doTest(): void {
// kto wywołuje?
print "parent :\n";
var_dump($this);
// widok nadrzędny
print "parent : attributeOfParent={$this->attributeOfParent}\n";
print "parent : attributeOfChild={$this->attributeOfChild}\n";
}
}
class SomeChild extends SomeParent {
// atrybut
private $attributeOfChild = 14;
// metoda
public function doTest(): void {
// widok podrzędny
print "child : attributeOfParent={$this->attributeOfParent}\n";
print "child : attributeOfChild={$this->attributeOfChild}\n";
// element nadrzędny
parent::doTest();
}
}
// skrypt główny
print "---------------------------test1\n";
(new SomeParent())->doTest();
print "---------------------------test2\n";
(new SomeChild())->doTest();
Wyniki
Uwagi
- wiersz 12: klasa [SomeChild] widzi teraz atrybut swojego obiektu nadrzędnego [$attributeOfParent]. Jest to normalne, ponieważ ten ostatni ma teraz zakres [protected];
- w metodzie [someParent::doTest] obiekt [$this] jest typu [SomeChild] (wiersze 15–20). Widzi on atrybut swojego obiektu nadrzędnego [$attributeOfparent] (wiersz 21), ale nadal nie widzi swojego własnego atrybutu [$attributeOfChild] (wiersze 23–28);
W trzeciej próbie atrybut [$attributeOfChild] również ma zakres [protected]:
<?php
class SomeParent {
// atrybut
protected $attributeOfParent = 4;
// metoda
public function doTest(): void {
// kto wywołuje?
print "parent :\n";
var_dump($this);
// widok nadrzędny
print "parent : attributeOfParent={$this->attributeOfParent}\n";
print "parent : attributeOfChild={$this->attributeOfChild}\n";
}
}
class SomeChild extends SomeParent {
// atrybut
protected $attributeOfChild = 14;
// metoda
public function doTest(): void {
// widok podrzędny
print "child : attributeOfParent={$this->attributeOfParent}\n";
print "child : attributeOfChild={$this->attributeOfChild}\n";
// element nadrzędny
parent::doTest();
}
}
// skrypt główny
print "---------------------------test1\n";
(new SomeParent())->doTest();
print "---------------------------test2\n";
(new SomeChild())->doTest();
Wyniki wykonania są następujące:
- wiersz 22: tym razem wewnątrz elementu nadrzędnego, [$this] typu [SomeChild] (wiersze 15–20) widzi chroniony atrybut [$attributeOfChild] swojej własnej klasy [SomeChild].
Jakie wnioski można wyciągnąć z tych testów?
- że instancja klasy nadrzędnej [$this], wykorzystywana w metodzie klasy nadrzędnej, widzi:
- atrybuty i metody klasy nadrzędnej, niezależnie od ich widoczności;
- nie widzi żadnych atrybutów ani metod swoich klas potomnych;
Jest to oczekiwane zachowanie.
- że instancja klasy potomnej [$this], wykorzystywana w metodzie klasy potomnej, widzi:
- atrybuty i metody klasy nadrzędnej, o ile mają one co najmniej widoczność [protected]. Te, które mają widoczność [private], nie są widoczne;
- atrybuty i metody klasy potomnej, niezależnie od ich widoczności;
Jest to oczekiwane zachowanie.
- instancja klasy potomnej o nazwie [$this], wykorzystywana w metodzie klasy nadrzędnej, widzi:
- atrybuty i metody klasy nadrzędnej, niezależnie od ich widoczności;
- atrybuty i metody własnej klasy tylko wtedy, gdy mają co najmniej widoczność [protected]. Te, które mają widoczność [private], nie są widoczne;
Jest to zachowanie nieoczekiwane.
5.19. Kodowanie klasy jSON
W klasie często występuje metoda [__toString]: ma ona zwracać ciąg znaków reprezentujący obiekt, który ją wywołuje. Kuszące może być, aby ten ciąg był ciągiem jSON. Teraz zbadamy tę ścieżkę.

Wykorzystamy następującą klasę [Personne]:
<?php
class Personne {
// atrybuty
private $nom;
private $prénom;
private $âge;
private $enfants;
// ustawianie globalne
public function setFromArray(array $arrayOfAttributes): Personne {
// inicjalizacja niektórych atrybutów klasy
foreach ($arrayOfAttributes as $attribute => $value) {
$this->$attribute = $value;
}
// zwracamy obiekt
return $this;
}
// metody pobierające
public function getNom() {
return $this->nom;
}
public function getPrénom() {
return $this->prénom;
}
public function getÂge() {
return $this->âge;
}
public function getEnfants() {
return $this->enfants;
}
// __toString
public function __toString(): string {
// identyfikacja obiektu
var_dump($this);
// pobieramy jego atrybuty
$attributes = \get_object_vars($this);
var_dump($attributes);
// zwracamy ciąg znaków jSON zawierający atrybuty
return \json_encode($attributes, JSON_UNESCAPED_UNICODE);
}
}
Komentarze
- wiersze 5–8: cztery atrybuty klasy;
- wiersze 20–35: metody pobierające, które pozwalają uzyskać wartości tych atrybutów;
- wiersze 11–18: globalna metoda setter, która pozwala zainicjować atrybuty na podstawie tablicy asocjacyjnej [$arrayOfAttributes], której klucze są identyczne z atrybutami klasy;
- wiersze 38–46: metoda [__toString] klasy;
- wiersz 42: funkcja PHP [get_object_vars] pozwala uzyskać wartości atrybutów klasy w postaci tablicy asocjacyjnej [‘nazwisko’=>’nazwisko1’, ‘imię’=>’imię1’, ‘wiek’=>wiek1, ‘dzieci’=>[]];
- wiersz 45: zwracamy ciąg znaków jSON z tej tablicy atrybutów;
Przyjrzyjmy się skryptowi [json-01.php], który wykorzystuje klasę [Personne]:
<?php
// klasa Osoba
require "Personne.php";
// instancjonowanie ojca
$père = new Personne();
// inicjalizacja klasy nadrzędnej
$père->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Dieudonné",
"âge" => 58
]);
// utworzenie instancji i inicjalizacja potomka1
$enfant1 = (new Personne())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Sylvain",
"âge" => 17
]);
// utworzenie instancji i inicjalizacja potomka2
$enfant2 = (new Personne())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Géraldine",
"âge" => 12
]);
// inicjalizacja potomków klasy nadrzędnej
$père->setFromArray([
"enfants" => [$enfant1, $enfant2]
]);
// wyświetlanie elementów obiektu nadrzędnego
$enfant1=($père->getEnfants())[0];
$enfant2=($père->getEnfants())[1];
print "------------------------enfant1\n";
print "enfant1=$enfant1\n";
print "------------------------enfant2\n";
print "enfant2=$enfant2\n";
print "------------------------père\n";
print "père=$père\n";
Komentarze
- wiersze 6–13: inicjujemy obiekt [Personne] [$père] za pomocą metody [Personne::setFromArray], która pozwala nazainicjować obiekt [Personne] za pomocą tablicy zawierającej klucze identyczne z atrybutami klasy [Personne];
- wiersze 14–19: w ten sam sposób inicjuje się obiekt [Personne] [$enfant1];
- wiersze 21–25: inicjuje się obiekt [Personne] [$enfant2];
- wiersze 27–29: inicjujemy atrybut [$père→enfants] za pomocą tablicy zawierającej dwoje dzieci;
- wiersze 32–33: przypisuje się oba elementy potomne do elementu nadrzędnego;
- wiersz 35: operacja [print] próbuje przekształcić obiekt [$enfant1] w ciąg znaków. W tym celu wykorzystuje metodę [__toString] tego obiektu. Oczekujemy zatem, że pojawi się ciąg znaków jSON obiektu;
- wiersze 38–39: to samo robimy z obiektem nadrzędnym;
Wyniki są następujące:
------------------------enfant1
object(Personne)#2 (4) {
["nom":"Personne":private]=>
string(11) "Bertholomé"
["prénom":"Personne":private]=>
string(7) "Sylvain"
["âge":"Personne":private]=>
int(17)
["enfants":"Personne":private]=>
NULL
}
array(4) {
["nom"]=>
string(11) "Bertholomé"
["prénom"]=>
string(7) "Sylvain"
["âge"]=>
int(17)
["enfants"]=>
NULL
}
enfant1={"nom":"Bertholomé","prénom":"Sylvain","âge":17,"enfants":null}
------------------------enfant2
object(Personne)#3 (4) {
["nom":"Personne":private]=>
string(11) "Bertholomé"
["prénom":"Personne":private]=>
string(10) "Géraldine"
["âge":"Personne":private]=>
int(12)
["enfants":"Personne":private]=>
NULL
}
array(4) {
["nom"]=>
string(11) "Bertholomé"
["prénom"]=>
string(10) "Géraldine"
["âge"]=>
int(12)
["enfants"]=>
NULL
}
enfant2={"nom":"Bertholomé","prénom":"Géraldine","âge":12,"enfants":null}
------------------------père
object(Personne)#1 (4) {
["nom":"Personne":private]=>
string(11) "Bertholomé"
["prénom":"Personne":private]=>
string(10) "Dieudonné"
["âge":"Personne":private]=>
int(58)
["enfants":"Personne":private]=>
array(2) {
[0]=>
object(Personne)#2 (4) {
["nom":"Personne":private]=>
string(11) "Bertholomé"
["prénom":"Personne":private]=>
string(7) "Sylvain"
["âge":"Personne":private]=>
int(17)
["enfants":"Personne":private]=>
NULL
}
[1]=>
object(Personne)#3 (4) {
["nom":"Personne":private]=>
string(11) "Bertholomé"
["prénom":"Personne":private]=>
string(10) "Géraldine"
["âge":"Personne":private]=>
int(12)
["enfants":"Personne":private]=>
NULL
}
}
}
array(4) {
["nom"]=>
string(11) "Bertholomé"
["prénom"]=>
string(10) "Dieudonné"
["âge"]=>
int(58)
["enfants"]=>
array(2) {
[0]=>
object(Personne)#2 (4) {
["nom":"Personne":private]=>
string(11) "Bertholomé"
["prénom":"Personne":private]=>
string(7) "Sylvain"
["âge":"Personne":private]=>
int(17)
["enfants":"Personne":private]=>
NULL
}
[1]=>
object(Personne)#3 (4) {
["nom":"Personne":private]=>
string(11) "Bertholomé"
["prénom":"Personne":private]=>
string(10) "Géraldine"
["âge":"Personne":private]=>
int(12)
["enfants":"Personne":private]=>
NULL
}
}
}
père={"nom":"Bertholomé","prénom":"Dieudonné","âge":58,"enfants":[{},{}]}
Komentarze
- wiersze 2–11: obiekt [$enfant1];
- wiersze 12–21: tablica atrybutów obiektu [$enfant1]. Mamy je wszystkie;
- wiersz 22: mamy ciąg znaków jSON obiektu [$enfant1];
- wiersze 23–44: to samo dotyczy obiektu [$enfant2];
- wiersze 45–112: w przypadku ojca sytuacja wygląda nieco inaczej, ponieważ jego atrybut [enfants] nie jest równy NULL, jak miało to miejsce w przypadku dzieci;
- wiersz 112: widać, że w ciągu znaków jSON ojca brakuje dzieci;
- wiersze 79–111: widać, że w tabeli atrybutów ojca dziecko 1 (wiersze 89–98) pozostało obiektem, podobnie jak dziecko 2 (wiersze 99–110). Mówiąc prościej, wyrażenie [\get_object_vars($this)], gdzie [$this] reprezentuje ojca, nie jest rekurencyjne: jeśli atrybut klasy [Personne] sam w sobie jest obiektem, wyrażenie [\get_object_vars($this)] nie próbuje uzyskać jego tablicy atrybutów;
Można to poprawić. Modyfikujemy klasę [Personne], przekształcając ją w następującą klasę [Personne2]:
<?php
class Personne2 {
// atrybuty
private $nom;
private $prénom;
private $âge;
private $enfants;
// ustawianie globalne
public function setFromArray(array $arrayOfAttributes): Personne2 {
…
// zwracamy obiekt
return $this;
}
// metody pobierające
public function getNom() {
return $this->nom;
}
…
// __toString
public function __toString(): string {
// pobieranie atrybutów obiektu
$attributes = $this->getAttributes($this);
$enfants = $attributes["enfants"];
if ($enfants != NULL) {
$attributes["enfants"] = [$enfants[0]->getAttributes(), $enfants[1]->getAttributes()];
}
// zwracamy ciąg znaków JSON zawierający atrybuty
return \json_encode($attributes, JSON_UNESCAPED_UNICODE);
}
public function getAttributes(): array {
return \get_object_vars($this);
}
}
Komentarze
- wiersze 36–38: funkcja [getAttributes] zwraca tablicę atrybutów obiektu, który ją wywołuje;
- wiersze 25–34: funkcja [__toString];
- wiersz 27: pobieramy atrybuty klasy [Personne] do tablicy [$attributes];
- wiersz 28: z poprzedniego przykładu wynika, że [$attributes["enfants"]] jest tablicą dwóch obiektów typu [Personne];
- wiersze 29–31: oba obiekty są zastępowane przez ich tablicę atrybutów;
- wiersz 33: pozostaje już tylko zakodować utworzoną tablicę atrybutów jako jSON;
Skrypt [json-02.php] wykorzystuje klasę [Personne2] w następujący sposób:
<?php
// klasa Personne2
require "Personne2.php";
// instancjonowanie klasy nadrzędnej
$père = new Personne2();
// inicjalizacja
$père->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Dieudonné",
"âge" => 58
]);
// utworzenie instancji i inicjalizacja elementu potomnego 1
$enfant1 = (new Personne2())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Sylvain",
"âge" => 17
]);
// utworzenie instancji i inicjalizacja potomka2
$enfant2 = (new Personne2())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Géraldine",
"âge" => 12
]);
// inicjalizacja potomków klasy nadrzędnej
$père->setFromArray([
"enfants" => [$enfant1, $enfant2]
]);
// wyświetlenie obiektu nadrzędnego
print "------------------------père\n";
print "père=$père\n";
Skrypt [json-02.php] jest identyczny ze skryptem [json-01.php], z tą różnicą, że klasa [Personne2] zastąpiła klasę [Personne].
Wyniki wykonania są następujące:
Tym razem udało się uzyskać dzieci wraz z ojcem.
Poprzednie rozwiązanie nie jest zadowalające, ponieważ dzieci mogą same mieć dzieci. W ten sposób powraca poprzedni problem.
Klasa [Personne3] rozwiązuje ten problem w następujący sposób:
<?php
class Personne3 {
// atrybuty
private $nom;
private $prénom;
private $âge;
private $enfants;
// globalny setter
public function setFromArray(array $arrayOfAttributes): Personne3 {
…
}
// metody pobierające
…
// __toString
public function __toString(): string {
// tworzy ciąg znaków JSON z atrybutów
$attributes = [];
$this->getRecursiveAttributes($attributes, $this, []);
// ciąg znaków JSON atrybutów
return \json_encode($attributes, JSON_UNESCAPED_UNICODE);
}
public function getAttributes(): array {
return \get_object_vars($this);
}
private function getRecursiveAttributes(array &$attributes, $value, $keys): void {
// analiza wartości [$value]
// $keys jest tablicą [key1, key2, .., keyn]
// $value=$attributesQZXW2HTMLBW2tleTFdZQXQZXW2HTMLBW2tleTJdZQX….[keyn]
// jeśli [$value] jest obiektem, używa się jego metody [getAttributes]
if (\is_object($value)) {
// atrybuty obiektu [$value]
$objectAttributes = $value->getAttributes();
// co robimy z wynikiem?
if ($keys) {
// w [$attributes] zamienimy $value na tablicę jego atrybutów
// należy skonstruować element $attributesQZXW2HTMLBW2tleTFdZQXQZXW2HTMLBW2tleTJdZQX…[keyn]
//, gdzie $keys jest tablicą [key1, key2, .., keyn]
// pobieramy odwołanie do tablicy [$attributes]
$attribute = &$attributes;
// przeszukuje się tablicę kluczy
foreach ($keys as $key) {
// pobieramy odwołanie do atrybutu
$attribute = &$attribute[$key];
}
// tutaj $attribut oraz $attributesQZXW2HTMLBW2tleTFdZQXQZXW2HTMLBW2tleTJdZQX…[key(n)] są identyczne
// i zajmują tę samą lokalizację w pamięci
// obiekt [$value] zostaje zastąpiony przez swoją tablicę atrybutów;
// należy wpisać $attributesQZXW2HTMLBW2tleTFdZQXQZXW2HTMLBW2tleTJdZQX…[keyn]=$objectAttributes
// co odpowiada $attribute = $objectAttributes
$attribute = $objectAttributes;
} else {
// brak kluczy – jesteśmy na początku badania obiektu
// $objectAttributes reprezentuje atrybuty pierwszego poziomu klasy
$attributes += $objectAttributes;
}
// być może w [$objectAttributes] nadal znajdują się obiekty
// badamy atrybuty obiektu [$objectAttributes]
$this->getRecursiveAttributes($attributes, $objectAttributes, $keys);
} else {
if (\is_array($value)) {
// mamy tablicę – analizujemy każdy z jej elementów
foreach ($value as $key => $élément) {
// dodajemy bieżący klucz do tablicy $keys
\array_push($keys, $key);
// analizujemy $élément
$this->getRecursiveAttributes($attributes, $élément, $keys);
// usuwamy z tablicy $keys klucz, który właśnie został przeanalizowany
\array_pop($keys);
}
}
}
}
Komentarze
- wiersze 21–22: tym razem metoda [__toString] pobiera atrybuty swojej klasy i żąda, aby operacja ta została wykonana rekurencyjnie: jeśli atrybut jest obiektem lub tablicą obiektów, wówczas każdy obiekt musi zostać zastąpiony swoją tablicą atrybutów w końcowej tablicy atrybutów klasy;
- wiersze 31–78: funkcja [getRecursiveAttributes] wykonuje to zadanie. Kod tej funkcji został opatrzony komentarzami. Pisanie funkcji rekurencyjnej jest często skomplikowane. Tak jest również w tym przypadku. Czytelnik nic nie straci, jeśli tego nie zrozumie. Istnieją biblioteki, które wykonują tę pracę. Wywołanie rekurencyjne ma miejsce w wierszach 64 i 72;
- zaletą tego kodu jest to, że nie został napisany wyłącznie dla klasy [Personne3]. Działa on dla każdej klasy posiadającej atrybuty, których wartościami są różne typy obiektów, o ile klasy używane przez klasę główną posiadają, podobnie jak ona, metodę [getAttributes] z wierszy 27–29
Skrypt [json-03.php] wykorzystuje klasę [Personne3] w następujący sposób:
<?php
// klasa Personne3
require "Personne3.php";
// instancjonowanie klasy nadrzędnej
$père = new Personne3();
// inicjalizacja
$père->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Dieudonné",
"âge" => 58
]);
// utworzenie instancji i inicjalizacja dziecka1
$enfant1 = (new Personne3())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Sylvain",
"âge" => 27
]);
// utworzenie instancji i inicjalizacja potomka2
$enfant2 = (new Personne3())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Géraldine",
"âge" => 12
]);
// inicjalizacja potomków klasy nadrzędnej
$père->setFromArray([
"enfants" => [$enfant1, $enfant2]
]);
// utworzenie instancji i inicjalizacja potomka11
$enfant11 = (new Personne3())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Gaëtan",
"âge" => 2
]);
// utworzenie instancji i inicjalizacja elementu potomnego 12
$enfant12 = (new Personne3())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Mathilde",
"âge" => 1
]);
// inicjalizacja elementów potomnych elementu potomnego 1
$enfant1->setFromArray([
"enfants" => [$enfant11, $enfant12]
]);
// wyświetlanie obiektu nadrzędnego
print "------------------------père\n";
print "père=$père\n";
- wiersze 30–45: klasie [$enfant1] przypisuje się dwa elementy potomne;
Wyniki wykonania są następujące:
Po sformatowaniu tego wyniku otrzymujemy następujący zapis:
père={
"nom": "Bertholomé",
"prénom": "Dieudonné",
"âge": 58,
"enfants": [
{
"nom": "Bertholomé",
"prénom": "Sylvain",
"âge": 27,
"enfants": [
{
"nom": "Bertholomé",
"prénom": "Gaëtan",
"âge": 2,
"enfants": null
},
{
"nom": "Bertholomé",
"prénom": "Mathilde",
"âge": 1,
"enfants": null
}
]
},
{
"nom": "Bertholomé",
"prénom": "Géraldine",
"âge": 12,
"enfants": null
}
]
}
Udało się poprawnie odzyskać ciąg znaków jSON ze wszystkich obiektów [Personne] tworzących obiekt nadrzędny.