Skip to content

5. Класи

Словник: Клас (class) — це тип PHP. Змінна цього типу називається об’єктом. Об’єкт — це екземпляр класу.

5.1. Дерево скриптів

Image

5.2. Будь-яка змінна може стати об’єктом, наділеним атрибутами

Скрипт [classes-01.php] має такий вигляд:


<?php

// загальний об’єкт
// $obj1=new stdClass();
// будь-яка змінна за своєю суттю може мати атрибути
$obj1->attr1 = "un";
$obj1->attr2 = 100;
// відображає об’єкт
print "objet1=[$obj1->attr1,$obj1->attr2]\n";
// змінює об’єкт
$obj1->attr2 += 100;
// виводить об’єкт
print "objet1=[$obj1->attr1,$obj1->attr2]\n";
// копіює значення об’єкта1 (адресу об’єкта, на який вказує покажчик) в об’єкт2
// обидві змінні тепер різні, але вказують на один і той самий об’єкт
$obj2 = $obj1;
// змінює об’єкт2
$obj2->attr2 = 0;
// виводить обидва об’єкти
print "objet1=[$obj1->attr1,$obj1->attr2]\n";
print "objet2=[$obj2->attr1,$obj2->attr2]\n";
// змінює об’єкт, на який вказує obj1
$obj1 = new stdClass();
print "obj1 :\n";
print_r($obj1);
print "obj2 :\n";
print_r($obj2);
// присвоює посилання (адресу) об’єкта2 об’єкту3
// $obj2 та $obj3 тепер є однією й тією самою змінною
$obj3 = &$obj2;
print "obj2 :\n";
print_r($obj2);
print "obj3 :\n";
print_r($obj3);
// змінює obj3
$obj3->attr2 = 10;
// виводить обидва об’єкти
print "objet2=[$obj2->attr1,$obj2->attr2]\n";
print "objet3=[$obj3->attr1,$obj3->attr2]\n";
// змінює об’єкт, на який вказує obj2
$obj2 = new stdClass();
$obj2->attr3 = "deux";
$obj2->attr4 = 20;
// виводить обидва об’єкти $obj2 та $obj3
print "obj2 :\n";
print_r($obj2);
print "obj3 :\n";
print_r($obj3);

// чи є об’єкт словником?
print count($obj3) . "\n";
while (list($attribut, $valeur) = each($obj3)) {
  print "obj3[$attribut]=$valeur\n";
}
// кінець
exit;

Результати:


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

Коментарі

  • рядок 6: запис $obj->attr позначає атрибут attr змінної $obj. Якщо його не існує, він створюється, перетворюючи змінну $obj на об’єкт з атрибутами. Ми бачили, що PHP створює за замовчуванням об’єкт типу stdClass;
  • рядок 16: вираз $obj2=$obj1, коли $obj1 є об’єктом, є копіюванням об’єктів за посиланням: $obj2 та $obj1 є посиланнями (адресами) на один і той самий об’єкт. Сам об’єкт може бути змінений за допомогою будь-якого з цих посилань;
  • рядки 23–27: мають на меті показати, що $obj1 та $obj2 — це дві різні змінні: вони не знаходяться за однією й тією ж адресою в пам’яті:
    • $obj2=$obj1 скопіювало значення $obj1 у змінну $obj2 (операція 1 вище). Значення $obj1 — це адреса об’єкта. Отже, $obj1 і $obj2 вказують на один і той самий об’єкт. Коли ми оперуємо змінною $obj, яка вказує на об’єкт, PHP оперує об’єктом, на який вказує змінна $obj. Згідно зі схемою нижче, бачимо, що об’єкт, на який вказує змінна, можна змінити або через $obj1, або через $obj2. Саме це показують рядки 4 та 5 результатів;

Image

  • рядок 30: вираз $obj3=&$obj2 призводить до того, що $obj2 та $obj3 мають одну й ту саму адресу [1 ci-dessous]. Можна сказати, що обидві змінні є псевдонімами одного й того самого місця в пам’яті. Вони обидві вказують на один об’єкт, Об’єкт А, наведений нижче: [2];
    • операція $obj2=new stdClass() призводить до створення нового об’єкта, Об’єкта B ([3 ci-dessous]), і адреса цього нового об’єкта присвоюється змінній $obj2. Оскільки $obj2 та $obj3 є двома псевдонімами одного й того самого місця в пам’яті, $obj3 також вказує на новий об’єкт «Об’єкт B». Це показано в рядках 16–27 та 30–41 результатів;

Image

  • рядки 52–54: показують, що об’єкт можна обходити як словник. Ключами словника є імена атрибутів, а значеннями — значення цих самих атрибутів;
  • рядок 51: функцію count можна застосувати до об’єкта (з появою попередження), але вона не повертає, як можна було б очікувати, кількість атрибутів. Отже, об’єкт має схожість зі словником, але не є ним;

5.3. Клас «Personne» без оголошених атрибутів

Скрипт [classes-02.php] має такий вигляд:


<?php

class Personne {

  // атрибути класу
  // не оголошені — можуть створюватися динамічно
  // метод
  function identite() {
    // апріорі використовує атрибути, яких не існує
    return "[$this->prenom,$this->nom,$this->age]";
  }

}

// тест
// атрибути є загальнодоступними і можуть створюватися динамічно
$p = new Personne();
$p->prenom = "Paul";
$p->nom = "Langevin";
$p->age = 48;
// виклик методу
print "personne=" . $p->identite() . "\n";
// кінець
exit;

Результати:

personne=[Paul,Langevin,48]

Коментарі

  • рядки 3–13: визначають клас Personne. Клас — це шаблон, на основі якого створюються об’єкти. Він об’єднує атрибути та функції, які називаються методами. Оголошувати атрибути не обов’язково;
  • рядки 8–11: метод identité виводить значення трьох атрибутів, не оголошених у класі. Ключове слово $this позначає об’єкт, до якого застосовується метод;
  • рядок 17: створюється об’єкт $p типу Personne. Ключове слово new використовується для створення нового об’єкта. Ця операція повертає посилання на створений об’єкт (тобто адресу). Можливі різні варіанти запису: new Personne(), new Personne, new personne. У назві класу не враховується регістр;
  • рядки 18–20: у об’єкті $p створюються три атрибути, необхідні для методу identité;
  • рядок 22: метод identité класу Personne застосовується до об’єкта $p. У коді (рядки 8–11) методу identité $this посилається на той самий об’єкт, що й $p;

5.4. Клас «Personne» з оголошеними атрибутами

Скрипт [classes-03.php] має такий вигляд:


<?php

class Personne {

  // атрибути класу
  var $prenom;
  var $nom;
  var $age;

  // метод
  function identite() {
    return "[$this->prenom,$this->nom,$this->age]";
  }

}

// тест
// атрибути є загальнодоступними
$p = new Personne();
$p->prenom = "Paul";
$p->nom = "Langevin";
$p->age = 48;
// виклик методу
print "personne=" . $p->identite() . "\n";
// кінець
exit;

Результати:

personne=[Paul,Langevin,48]

Коментарі

  • рядки 6–8: атрибути класу явно оголошуються за допомогою ключового слова var;

5.5. Клас Personne з конструктором

Попередні приклади демонстрували нестандартні класи, такі як Personne, які можна було знайти в PHP 4. Не рекомендується слідувати цим прикладам. Тепер ми представимо клас Personne [classes-04.php], що відповідає найкращим практикам PHP 7:


<?php

// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);
class Personne {
// атрибути класу
  private $prenom;
  private $nom;
  private $age;

// методи getter та setter
  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;
  }

// конструктор
  public function __construct(string $prenom, string $nom, int $age) {
    // використовуються методи set
    $this->setPrenom($prenom);
    $this->setNom($nom);
    $this->setAge($age);
  }

// метод toString
  public function __toString(): string {
    return "[$this->prenom,$this->nom,$this->age]";
  }

}

// тест
// створення об’єкта «Особа»
$p = new Personne("Paul", "Langevin", 48);
// ідентифікаційні дані цієї особи
print "personne=$p\n";
// зміна віку
$p->setAge(14);
// ідентифікаційні дані особи
print "personne=$p\n";
// кінець
exit;

Результати:

personne=[Paul,Langevin,48]
personne=[Paul,Langevin,14]

Коментарі

  • рядки 6–50: клас Personne;
  • рядки 7–9: приватні (private) атрибути класу. Ці атрибути видимі лише всередині класу. Інші ключові слова, які можна використовувати:
  • public: робить атрибут public видимим ззовні класу,
  • protected: робить атрибут protégé видимим зсередини класу та класів, похідних від нього;
  • оскільки атрибути є приватними, доступ до них ззовні класу неможливий. Тому не можна написати такий код:
$p=new Personne() ;
$p->nom="Landru" ;

Тут ми знаходимося поза класом Personne. Оскільки атрибут nom є приватним, рядок 2 є некоректним. Щоб ініціалізувати приватні поля об’єкта $p, є два способи:

  • використати публічні методи set та get (назви цих методів можуть бути будь-якими) у рядках 12–34. Тоді можна написати:
$p=new Personne() ;
$p->setNom("Landru") ;
  • використати конструктор із рядків 37–42. Тоді можна написати:
$p=new Personne("Michel","Landru",44) ;

Наведений вище запис автоматично викликає метод класу Personne, який називається __construct;

  • рядок 59: цей рядок виводить об’єкт $p у вигляді символьного рядка. Для цього використовується метод класу Personne, який називається __toString (рядки 45–47);
  • усі методи класу (функції) мають префікс ключового слова public, що вказує на те, що функція є видимою поза межами класу. Іншими ключовими словами, які можна використовувати, є, як і для атрибутів та з тим самим значенням, private та protected. Без явного атрибута видимості функція має неявну видимість public;

5.6. Клас Personne з перевірками правильності в конструкторі

Конструктор класу — це ідеальне місце для перевірки правильності значень ініціалізації об’єкта. Однак об’єкт також можна ініціалізувати за допомогою методів set або їхніх еквівалентів. Щоб уникнути дублювання одних і тих самих перевірок у двох різних місцях, їх можна розмістити в методах set. Якщо значення ініціалізації об’єкта виявиться неправильним, буде згенеровано виняток. Ось приклад.

Спочатку перемістимо визначення класу Personne у власний файл [Personne.php]:


<?php

// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);

// простір імен;
namespace Exemples;

// клас «Особа»
class Personne {
// атрибути класу
  private $prenom;
  private $nom;
  private $age;

// методи getter та setter
  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 {
    // ім'я має бути не порожнім
    $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 {
    // прізвище не може бути порожнім
    $nom = trim($nom);
    if ($nom === "") {
      throw new \Exception("Le nom doit être non vide");
    } else {
      $this->nom = $nom;
    }
  }

  public function setAge(int $âge): void {
    // вік має бути дійсним
    if ($âge < 0) {
      throw new \Exception("L'âge doit être un entier positif ou nul");
    } else {
      $this->age = $âge;
    }
  }

// конструктор
  public function __construct(string $prenom, string $nom, int $age) {
    // використовуються методи set
    $this->setPrenom($prenom);
    $this->setNom($nom);
    $this->setAge($age);
  }

  // метод
  public function initWithPersonne(Personne $p): void {
    // ініціалізує поточний об’єкт з особою $p
    $this->__construct($p->prenom, $p->nom, $p->age);
  }

  // метод toString
  function __toString(): string {
    return "[$this->prenom,$this->nom,$this->age]";
  }

}

Коментарі

  • рядок 4: вимагається, щоб тип параметрів функцій відповідав заявленому;
  • рядок 7: визначає простір імен (namespace). Повне ім’я (qualifié) класу Personne тепер становить \Exemples\Personne. Зверніть увагу на символ \, з якого починається кваліфікована назва: у цьому випадку маємо кваліфіковану назву absolu. Якщо цей символ відсутній, маємо кваліфіковану назву relatif (відносно поточного простору імен). Отже, якщо два класи A і B належать до одного й того самого простору імен E, у коді класу A можна звернутися до класу B за допомогою запису relative B. Якщо клас A належить до простору імен E1, а клас B — до простору імен E2, то в коді класу A до класу B можна звернутися за допомогою запису absolue \E2\B. Визначення класу всередині простору імен не є обов’язковим, але NetBeans видає попередження, якщо цього не зробити. Тому ми це зробимо. Крім того, простори імен повинні відповідати структурі файлів. Так, клас A у просторі імен E1 повинен знаходитися у файлі E1/A.php. Це не є обов’язковим, але й у цьому випадку NetBeans видає попередження, якщо цього не зробити. На прикладі класу [\Exemples\Personne] NetBeans видає попередження, оскільки ієрархія файлів [Personne.php] має вигляд [exemples/classes/Personne.php] і, отже, не відповідає простору імен. Не слід плутати структуру каталогів із простором імен. Повнокваліфікована назва класу використовує простір імен і не має нічого спільного зі структурою каталогів файлу PHP цього класу. Зв’язок між структурою каталогів та простором імен є необов’язковим і може не дотримуватися, як ми це зробили тут;
  • рядки 12–14: три приватні атрибути класу;
  • рядки 29–37: ініціалізація атрибута prenom та перевірка значення ініціалізації;
  • рядок 31: функція trim($chaine) видаляє пробіли, що знаходяться на початку та в кінці $chaine. Таким чином, trim(«abcd ») дає рядок «abcd», а trim(« ») — порожній рядок;
  • рядок 32: якщо ім’я порожнє, то генерується виняток (рядок 33), інакше ім’я зберігається (рядок 35). Для генерації винятку тут використано попередньо визначений клас [Exception]. Тут доводиться використовувати його абсолютне ім’я [\Exception]. Якщо використовувати його відносне ім’я [Exception], то цей клас шукатиметься в поточному просторі імен, тобто в просторі імен [Exemples] класу Personne. Таким чином, інтерпретатор PHP шукатиме клас з абсолютним іменем [\Exemples\Exception], якого не існує;

Клас [Personne] використовується наступним скриптом [classes-05.php]:


<?php

// суворе дотримання типів параметрів функцій
declare(strict_types=1);

// включення визначення класу «Особа»
require_once __DIR__."/Personne.php";

// кваліфікована назва класу «Personne»
use \Exemples\Personne;

// тест
// створення об’єкта «Personne»
$p = new Personne("Paul", "Langevin", 48);
// ідентифікатор цієї особи
print "Exemple1, personne=$p\n";
// створення помилкового об’єкта «Personne»
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";
}
// помилкове створення об’єкта «Особа»
try {
  $p = new Personne("", "yy", 10);
} catch (\Exception $e1) {
  print "Exemple3, erreur : " . $e1->getMessage() . "\n";
} catch (\TypeError $e2) {
  print "Exemple3, erreur : " . $e2->getMessage() . "\n";
}

// кінець
exit;

Коментарі

  • рядок 7: скрипт використовуватиме клас [Personne]. Тому потрібно вказати інтерпретатору PHP, де він може знайти визначення цього класу. Саме для цього призначені команди [include fichier] та [require fichier]. У даному випадку було використано команду [include]. Різниця між цими двома інструкціями полягає в наступному: якщо інструкція [include fichier] стикається з помилками під час завантаження [fichier], видається помилка рівня [E_WARNING], алевиконання продовжується, тоді як [require] у тому ж випадку генерує фатальну помилку, і виконання скрипта зупиняється. Кожна з цих двох інструкцій має варіанти [include_once] та [require_once]. Ці два варіанти дозволяють обробляти випадки багаторазового включення одного й того самого файлу. Можна уявити собі проект, що складається з декількох скриптів PHP, де кілька з них посилаються на клас [Personne]. Їх виконання призведе до багаторазового включення файлу [Personne.php] і спричинить помилку, оскільки клас не може бути визначений двічі. Рішенням є використання варіантів [_once], які гарантують, що файл буде включено лише один раз у глобальний скрипт проєкту;
  • рядок 7: константа [__DIR__] є константою PHP, яка позначає повну назву папки, в якій знаходиться скрипт, що містить константу [__DIR__]. Таким чином, вираз у рядку 17:

require_once __DIR__."/Personne.php";

буде еквівалентним чомусь на кшталт:


require_once ‘C:\Data\st-2019\dev\php7\php5-exemples\exemples\classes/Personnes.php’

У шляху до файлу можна однаково використовувати знаки / та \;

  • рядок 14: використовується клас [Personne], який ми щойно визначили. Скрипт [classes-05.php] не має просторів імен. У рядку 14 використовується відносне ім’я класу [Personne] без простору імен. Оскільки клас [Personne] не має простору імен, його шукають у самому скрипті [classes-05.php], тому його не буде знайдено. Існує два рішення цієї проблеми:
    • використати повну назву класу [\Exemples\Personne];
    • використати оператор use у рядку 10. Він вказує, що код, який йде далі, використовує клас [\Exemples\Personne];
  • рядок 10: оператор use дозволяє інтерпретатору зрозуміти, що клас [Personne], на який є посилання в рядку 14, насправді є класом [\Exemples\Personne]. Однак де інтерпретатор знайде код цього класу? Відповідь міститься у рядку 7. У ньому вказано, що для виконання поточного скрипта необхідно також завантажити скрипт [Personne.php]. Тут використано відносне ім’я файлу. Отже, його шукатимуть у папці, що містить скрипт [classes-05.php]. Отже, скрипти [Personne.php] та [classes-05.php] мають бути в одній папці. У даному випадку це так: обидва вони знаходяться в папці [exemples/classes]. Інструкція в рядку 10 еквівалентна:

use \Exemples\Personne as Personne;

Наведена вище інструкція [use] вказує, що псевдонім [Personne] позначає клас [\Exemples\Personne];

  • рядок 14: створюється об’єкт [Personne]. Тут неявно буде виконано метод [__construct] класу [Personne];
  • рядок 16: відображає особу $p. Щоб значення змінної $p могло бути виведене на екран, його потрібно перетворити на рядок символів. Неявно при цьому виконується метод [Personne.__toString]. Отже, цей метод повинен повернути рядок символів;
  • ми бачили, що конструктор класу [Personne] може викликати виняток типу [\Exception]. Отже, це виключення потрібно обробити. Тому код у рядку 14 є неповним. Необхідно використати код із рядків 18–24, щоб правильно обробити виключення, яке може виникнути. Тут ми навмисно генеруємо виключення, передаючи вік, що не є цілим числом. У цьому конкретному випадку виняток, що виникає, генерується інтерпретатором PHP, а не кодом класу [Personne]. Дійсно, сигнатура методу [Personne.__construct] має такий вигляд:

function __construct(string $prenom, string $nom, int $age)

Отже, параметр [age], що передається конструктору, повинен бути цілочисельним. Якщо це не так, інтерпретатор PHP генерує помилку типу [TypeError]. Крім того, методи [set] класу [Personne], у свою чергу, генерують виняток типу [\Exception]. Оскільки конструктор, який їх викликає, не має структури try/catch, виняток передається на рівень вище — до коду, що викликав конструктор, тобто до коду скрипта [classes-05.php]. Зрештою, скрипт [classes-05.php] може отримати два типи винятків: \Exception або \TypeError. Слід зауважити, що коли розробник впевнений, що певні винятки не можуть виникнути, він не використовуватиме відповідні опції catch. Тут вони систематично використовуються виключно з метою демонстрації. Однак опції catch використовуватимуться для будь-якого можливого винятку, навіть малоймовірного;

З цієї причини структура try у рядках 18–24 має два оператори catch для окремого оброблення двох типів винятків;

  • рядок 20: можна написати як [Exception], так і [\Exception]:
    • у першому варіанті використовується відносне ім’я класу, відносне до простору імен скрипта. У скрипта немає власного простору імен. Тому його простір імен є кореневим простором імен: \. Отже, написати тут [Exception] — це те саме, що написати [\Exception]. Однак клас [Exception] дійсно знаходиться в просторі імен [\];

Краще використовувати абсолютне ім’я попередньо визначених винятків PHP у скрипті, який сам не має просторів імен. Отже, якщо вирішити надати цьому скрипту простір імен, використання абсолютних імен класів залишається правильним, тоді як в іншому випадку зміна простору імен спричинить помилки у відносних іменах класів;

  • рядок 21: у разі виникнення винятку метод [Exception→getMessage] дозволяє отримати повідомлення про помилку цього винятку. Те саме стосується помилки типу [TypeError]. У методі [Personne.setPrenom] написано:

  public function setPrenom(string $prénom) {
    // ім'я не може бути порожнім
    $prénom = trim($prénom);
    if ($prénom === "") {
      throw new \Exception("Le prénom doit être non vide");
    } else {
      $this->prenom = $prénom;
    }
}

У рядку 5 генерується виняток із повідомленням про помилку [Le prénom doit être non vide]. Саме це отримає метод [Exception→getMessage] у рядку 29 скрипта [classes-05.php].

Результати:


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. Додавання методу, що виконує роль другого конструктора

У PHP 7 неможливо мати кілька конструкторів з різними параметрами, які б дозволяли створювати об’єкт різними способами. Тому можна використовувати методи, що виконують роль конструктора:


  // метод
  public function initWithPersonne(Personne $p) {
    // ініціалізує поточний об’єкт з використанням особи $p
    $this->__construct($p->prenom, $p->nom, $p->age);
}

Коментарі

  • рядки 2–5: метод initWithPersonne дозволяє присвоїти поточному об’єкту значення атрибутів іншого об’єкта Personne. Тут він викликає конструктор __construct, але це не обов’язково. Він міг би самостійно ініціалізувати атрибути класу [Personne];

Новий клас [Personne] використовується у наступному скрипті [classes-06.php]:


<?php

// включення визначення класу «Особа»
require_once __DIR__."/Personne.php";
// оголошення кваліфікованого імені класу «Personne»
use \Exemples\Personne;

// тест
// створення об’єкта «Особа»
try {
  $p = new Personne("Paul", "Langevin", 48);
} catch (\Exception $e) {
  print "erreur : " . $e->getMessage();
  exit;
}
// ідентифікація цієї особи
print "personne=$p\n";
// створення другої особи
try {
  $p2 = new Personne("Laure", "Adeline", 67);
} catch (\Exception $e) {
  print "erreur : " . $e->getMessage();
  exit;
}
// ініціалізація першого об’єкта значеннями другого
try {
  $p->initWithPersonne($p2);
} catch (\Exception $e) {
  print "erreur : " . $e->getMessage();
  exit;
}

// перевірка
print "personne=$p\n";
// кінець
exit;
  • рядки 14, 23, 30: часто трапляється, що після виникнення винятку доводиться зупиняти виконання консольного скрипта, якщо помилка є непереборною. У веб-скрипті ситуація інша: виконання скрипта не зупиняється, а замість цього відображається сторінка з повідомленням про помилку. Якщо скрипт знаходиться у функції, використовується не оператор exit, а return: виконання скрипта не зупиняється (exit), а відбувається вихід із функції (return) після встановлення помилки;

Результати:

personne=[Paul,Langevin,48]
personne=[Laure,Adeline,67]

5.8. Масив об’єктів [Personne]

Наступний приклад [classes-07.php] показує, що можна мати масиви об'єктів.


<?php

require_once __DIR__."/Personne.php";
use \Exemples\Personne;

// тест
// створення масиву об’єктів «Особа»
// для полегшення розуміння коду можливе виключення не обробляється
$groupe = [new Personne("Paul", "Langevin", 48), new Personne("Sylvie", "Lefur", 70)];

// ідентифікаційні дані цих осіб
for ($i = 0; $i < count($groupe); $i++) {
  print "groupe[$i]=$groupe[$i]\n";
}

// кінець
exit;

Результати:

groupe[0]=[Paul,Langevin,48]
groupe[1]=[Sylvie,Lefur,70]

Коментарі

  • рядок 9: створення масиву з 2 осіб;
  • рядок 12: обхід масиву;
  • рядок 13: $groupe[$i] є об’єктом типу Personne. Для його відображення використовується метод [Personne.__toString];

5.9. Створення класу, похідного від класу Personne

У файлі [Enseignant.php] створюємо такий клас [Enseignant]:


<?php

// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);

// простір імен
namespace Exemples;

// похідний клас від класу «особа»
class Enseignant extends Personne {
  // атрибути
  private $discipline;   // предмет, що викладається

  // гетери та сеттери

  public function getDiscipline(): string {
    return $this->discipline;
  }

  public function setDiscipline(string $discipline): void {
    $this->discipline = $discipline;
  }

  // конструктор
  public function __construct(string $prénom, string $nom, int $âge, string $discipline) {
    // атрибути батьківського класу
    parent::__construct($prénom, $nom, $âge);
    // інші атрибути
    $this->setDiscipline($discipline);
  }

  // перевантаження функції __toString батьківського класу
  public function __toString(): string {
    return "[" . parent::__toString() . ",$this->discipline]";
  }

}

Коментарі

  • рядок 7: клас [Enseignant] також входить до простору імен [Exemples];
  • рядок 10: клас Enseignant походить (extends) від класу Personne.. Похідний клас Enseignant успадковує атрибути та методи свого батьківського класу;
  • рядок 12: клас Enseignant додає новий атрибут discipline, який є властивим лише йому;
  • рядок 25: конструктор класу Enseignant приймає 4 параметри:
    • 3 — для ініціалізації батьківського класу (ім’я, прізвище, вік), рядок 27;
    • 1 — для власної ініціалізації (предмет), рядок 29;
  • рядок 27: похідний клас має доступ до методів і конструкторів свого батьківського класу за допомогою ключового слова parent ::. Тут параметри (ім’я, прізвище, вік) передаються конструктору батьківського класу;
  • рядки 33–35: метод __toString похідного класу використовує метод __toString батьківського класу;

Клас [Enseignant] використовується у наступному скрипті [classes-08.php]:


<?php

// включення визначень обох класів
require_once __DIR__."/Personne.php";
require_once __DIR__."/Enseignant.php";
// оголошення двох використовуваних класів
use \Exemples\Personne;
use \Exemples\Enseignant;

// тест
// створення масиву об’єктів класу Personne та похідних
// для простоти прикладу винятки не обробляються
$groupe = array(new Enseignant("Paul", "Langevin", 48, "anglais"), new Personne("Sylvie", "Lefur", 70));

// ідентифікаційні дані цих осіб
for ($i = 0; $i < count($groupe); $i++) {
  print "groupe[$i]=$groupe[$i]\n";
}
// кінець
exit;

Коментарі

  • рядки 4–5: нам потрібно вказати інтерпретатору PHP, де знаходяться обидва класи [Enseignant, Personne];
  • рядки 7–8: оголошення повних імен обох класів. Це дозволить нам позначати їх у коді просто за іменем, без суфікса їхніх просторів імен;
  • рядок 13: створюємо масив, що містить тип [Personne] та тип [Enseignant];
  • рядки 16–18: виводять елементи масиву;
  • рядок 17: буде викликано метод __toString кожного елемента $groupe[$i]. Клас Personne має метод __toString. Клас Enseignant має два методи: метод свого батьківського класу та власний. Можна задатися питанням, який із них буде викликано. Виконання показує, що було викликано метод класу Enseignant. Так відбувається завжди: коли метод викликається на об’єкті, його шукають у такому порядку: у самому об’єкті, у його батьківському класі, якщо такий є, потім у батьківському класі батьківського класу тощо… Пошук припиняється, щойно метод знайдено.

Результати:

groupe[0]=[[Paul,Langevin,48],anglais]
groupe[1]=[Sylvie,Lefur,70]

5.10. Створення другого класу, похідного від класу «Personne»

У наведеному нижче прикладі у файлі [Etudiant.php] створюється клас Etudiant, похідний від класу Personne:


<?php

// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);

// простір імен
namespace Exemples;

class Etudiant extends Personne {
  // атрибути
  private $formation;   // пройдений курс навчання

  // геттери та сеттери
  public function getFormation(): string {
    return $this->formation;
  }

  public function setFormation(string $formation): void {
    $this->formation = $formation;
  }

  // конструктор
  public function __construct(string $prénom, string $nom, int $âge, string $formation) {
    // атрибути батьківського об’єкта
    parent::__construct($prénom, $nom, $âge);
    // інші атрибути
    $this->setFormation($formation);
  }

  // перевантаження функції __toString батьківського класу
  public function __toString(): string {
    return "[" . parent::__toString() . ",$this->formation]]";
  }

}

Цей клас використовується у наступному скрипті [classes-09.php]:


<?php

// включення та визначення класів, що використовуються скриптом
require_once __DIR__."/Personne.php";
use \Exemples\Personne;
require_once __DIR__."/Enseignant.php";
use \Exemples\Enseignant;
require_once __DIR__."/Etudiant.php";
use \Exemples\Etudiant;

// тест
// створення масиву об’єктів класу «особа» та похідних від нього
// для полегшення розуміння прикладу винятки не обробляються
$groupe = array(new Enseignant("Paul", "Langevin", 48, "anglais"), new Personne("Sylvie", "Lefur", 70), new Etudiant("Steve", "Boer", 23, "iup2 qualité"));

// ідентичність цих осіб
for ($i = 0; $i < count($groupe); $i++) {
  print "groupe[$i]=$groupe[$i]\n";
}
// кінець
exit;

Результати:


groupe[0]=[[Paul,Langevin,48],anglais]
groupe[1]=[Sylvie,Lefur,70]
groupe[2]=[[Steve,Boer,23],iup2 qualité]

5.11. Взаємозв’язок конструктора похідного класу з конструктором батьківського класу

У деяких об’єктно-орієнтованих мовах конструктор похідного класу автоматично викликає конструктор свого батьківського класу. Наступний код [classes-16.php] показує, що у випадку з PHP 7 це не так:


<?php

class Classe1 {

  // конструктор
  public function __construct() {
    print "constructeur de la classe Classe1\n";
  }

}

class Classe2 extends Classe1 {

  // конструктор
  public function __construct() {
    // конструктор батьківського класу не викликається неявно
    print "constructeur de la classe Classe2\n";
  }

}

class Classe3 extends Classe1 {

  // конструктор
  public function __construct() {
    // явний виклик конструктора батьківського класу
    parent::__construct();
    // власний код класу Classe3
    print "constructeur de la classe Classe3\n";
  }

}

// тести
print "test1---------\n";
new Classe2();
print "test2---------\n";
new Classe3();

Результати

1
2
3
4
5
test1---------
constructeur de la classe Classe2
test2---------
constructeur de la classe Classe1
constructeur de la classe Classe3

5.12. Перевизначення методу батьківського класу

Ми вже бачили, що метод батьківського класу можна перевизначити у дочірньому класі. Так, метод [__toString] класу [Personne] (див. посилання) було перевизначено у дочірніх класах [Enseignant] (див. посилання) та [Etudiant] (див. посилання). Скрипт [classes-13.php] знову ілюструє цю концепцію:


<?php

// суворе дотримання типів параметрів функцій
declare(strict_types=1);

// головний клас
class Classe1 {

  public function f(): int {
    return 1;
  }

  function g(): int {
    return 2;
  }

}

// похідний клас
class Classe2 extends Classe1 {

  // перевизначення функції f батьківського класу
  public function f(): int {
    return parent::f() + 10;
  }

}

// код
$c2 = new Classe2();
print $c2->f() . "\n";
print $c2->g() . "\n";
$c1=new Classe1();
print $c1->f()."\n";

Коментарі

  • рядки 7–17: клас [Classe1] визначає два методи f та g;
  • рядки 20–27: клас [Classe2] успадковує клас [Classe1] і перевизначає його метод f;

Результати

1
2
3
11
2
1

Коментарі

  • у рядку 30 коду створюється об’єкт $c2 типу [Classe2];
  • у рядку 31 коду викликається метод f об’єкта $c2. Оскільки цей метод існує, він виконується;
  • у рядку 32 коду викликається метод g об’єкта $c2. Оскільки цей метод не існує, його шукають у батьківському класі, де він знайдений і виконується;
  • у рядку 33 коду створюється об’єкт $c1 типу [Classe1];
  • у рядку 34 коду викликається метод f об’єкта $c1. Оскільки цей метод існує, він виконується;

5.13. Передача об’єкта як параметра функції

Розглянемо такий скрипт [classes-14.php]:


<?php

// суворе дотримання типів параметрів функцій
declare(strict_types=1);

// головний клас
class Classe1 {

  public function f(): int {
    return 1;
  }

  function g(): int {
    return 2;
  }

}

// похідний клас
class Classe2 extends Classe1 {

  // перевизначається функція f батьківського класу
  public function f(): int {
    return parent::f() + 10;
  }

}

// параметр функції має тип «Клас1» або похідний
function doSomething(Classe1 $c1): void {
  print $c1->f() + $c1->g() . "\n";
}

// код
// створюється об’єкт типу «Клас2», похідний від «Клас1»
$c2 = new Classe2();
// викликається doSomething з
doSomething($c2);

Коментарі

  • рядки 7–17: клас [Classe1];
  • рядки 20–27: клас [Classe2], похідний від [Classe1];
  • рядок 30: функція, яка очікує параметр типу [Classe1]. Якщо очікуваним типом є клас, то фактичним параметром може бути об’єкт очікуваного типу або похідного типу;
  • рядки 35–38: функція [doSomething] викликається з параметром типу [Classe2], тоді як очікується тип [Classe1];

Результати

13

5.14. Абстрактні класи

Абстрактний клас — це неповний клас, який не можна інстанціювати. Щоб його можна було використовувати, він обов’язково має бути похідним.

Для чого потрібен абстрактний клас? Іноді трапляються класи, які мають один або кілька спільних методів, але відрізняються іншими методами чи атрибутами. У такому разі доцільно об’єднати все спільне в батьківському класі. Наразі нам не потрібен абстрактний клас. Але припустимо, що дочірні класи відрізняються лише одним методом M: сигнатура методу буде однаковою у всіх дочірніх класах, але його реалізація буде різною. Щоб змусити дочірні класи реалізувати метод M:

  • ми оголосимо сигнатуру методу M у батьківському класі. Оскільки цей клас не знає, як його реалізувати, ми додаємо до методу префікс у вигляді ключового слова abstract: це означає, що реалізація методу M перекладається на дочірні класи;
  • оскільки батьківський клас не є повністю реалізованим, він також оголошується абстрактним за допомогою того самого ключового слова abstract. Це означає, що клас більше не може бути інстанційований. Необхідно обов’язково створити дочірній клас, який визначить реалізацію методу M, щоб тіло батьківського класу можна було використовувати;

Ось приклад [classes-15.php]:


<?php

// суворим дотриманням типів параметрів функцій
declare(strict_types=1);

// абстрактний головний клас
abstract Class Classe1 {

  // метод, відомий усім похідним класам
  public function f(): int {
    return 1;
  }

  // абстрактний метод g — буде визначений похідними класами
  abstract function g(): int;
}

// похідний клас
Class Classe2 extends Classe1 {

  // метод g батьківського класу має бути визначений
  public function g(): int {
    return parent::f() + 10;
  }

}

// похідний клас
Class Classe3 extends Classe1 {

  // метод g батьківського класу має бути визначений
  public function g(): int {
    return parent::f() + 20;
  }

  // метод f батьківського класу можна перевизначити
  public function f(): int {
    return 2;
  }

}

// код
$c2 = new Classe2();
print $c2->f() . "\n";
print $c2->g() . "\n";
$c3 = new Classe3();
print $c3->f() . "\n";
print $c3->g() . "\n";

Коментарі

  • рядки 7–16: клас [Classe1] є абстрактним (рядок 7), оскільки він не реалізує метод g із рядка 15. Тому для його використання обов’язково потрібно створити похідний клас;
  • рядки 19–26: клас [Classe2] успадковує клас [Classe1] і перевизначає метод g свого батьківського класу (рядки 22–24);
  • рядки 29–41: клас [Classe3] є похідним від класу [Classe1] і перевизначає метод g свого батьківського класу (рядки 32–34);
  • рядки 37–39: клас [Classe3] перевизначає метод f свого батьківського класу;
  • рядки 44–49: створюються два об’єкти типів [Classe2] та [Classe3], після чого викликаються їхні методи f та g;

Результати

1
2
3
4
1
11
2
21

5.15. Кінцеві класи

Кінцевий клас — це клас, від якого не можна створювати похідні класи. Розглянемо такий скрипт [classes-11.php]:


<?php

// простір імен
namespace Exemples;

// клас, що не може бути похідним
final Class Classe1 {
  
}

// похідний клас
Class Classe2 extends Classe1 {
  
}

// код — має викликати помилку
new Classe2();

Коментарі

  • рядки 7–9: ключове слово final робить клас [Classe1] фінальним класом, від якого не можна створювати похідні класи;
  • рядки 12–14: клас [Classe2] успадковує фінальний клас [Classe1], що є помилкою;
  • рядок 17: про помилку буде повідомлено лише під час виконання скрипта, коли буде зроблена спроба оперувати об’єктом типу [Classe2];

Результати

Fatal error: Class Exemples\Classe2 may not inherit from final class (Exemples\Classe1) in C:\Data\st-2019\dev\php7\php5-exemples\exemples\classes\classes-11.php on line 14

5.16. Фінальні методи

Фінальний метод — це метод, який не можна перевизначити шляхом успадкування. Ось приклад [classes-12.php]:


<?php

// суворе дотримання типів параметрів функцій
declare(strict_types=1);

// простір імен
namespace Exemples;

// головний клас
Class Classe1 {

  // цей метод не можна перевизначити у похідному класі
  public final function f(): int {
    return 1;
  }

}

// похідний клас
Class Classe2 extends Classe1 {

  public function f(): int {
    return 2;
  }

}

// код — має викликати помилку
new Classe2();

Коментарі

  • рядок 13: метод f класу [Classe1] оголошено фінальним за допомогою ключового слова final;
  • рядок 20: клас [Classe2] успадковує клас [Classe1];
  • рядки 22–23: переопределяется функція f батьківського класу [Classe1]. Це має спричинити помилку;
  • рядок 29: створюється об’єкт типу [Classe2], щоб змусити інтерпретатор PHP перевірити клас [Classe2];

Результати

Fatal error: Cannot override final method Exemples\Classe1::f() in C:\Data\st-2019\dev\php7\php5-exemples\exemples\classes\classes-12.php on line 26

5.17. Статичні методи та атрибути

Статичний метод — це метод, пов’язаний із класом, у якому він визначений, а не з об’єктами-інстанціями цього класу. Отже, якщо клас C оголошує статичний метод M, то для його використання слід написати:

  • C::M, якщо ми знаходимося поза класом;
  • self::M — якщо ми перебуваємо всередині класу;

Ось приклад [classes-17.php]:


<?php

class Classe1 {

  // статичний метод
  static function say(string $message): void {
    print "$message\n";
  }

}

// тест -------------------
Classe1::say("hello");

Коментарі

  • рядок 6: метод [say] оголошено статичним за допомогою ключового слова static;
  • рядок 13: виклик статичного методу [say] у такій нотації: Classe1::say;

Результати

hello

Розглянемо тепер такий код [classes-18.php]:


<?php

class Classe1 {
  // статичний атрибут
  private static $nbObjects = 0;

  public function __construct() {
    print "constructeur Classe1\n";
    self::$nbObjects++;
  }

  // статичний метод
  static function say(): void {
    print self::$nbObjects ." objets de type [Classe1] ont été construits\n";
  }

}

// тест -------------------
new Classe1();
new Classe1();
Classe1::say();

Коментарі

  • рядок 5: оголошується статичний атрибут, який буде підраховувати кількість створених екземплярів класу [Classe1]. Це не атрибут, який може належати екземпляру класу. Адже якщо створено два об’єкти O1 та O2, жоден із них не знає про існування іншого. Наявність лічильника в екземплярі не має сенсу: коли створюється новий об’єкт, у якому екземплярі ми будемо збільшувати лічильник? Нам довелося б збільшувати лічильник конкретного об’єкта, ігноруючи лічильники інших екземплярів. Статичний атрибут — це атрибут класу, а не екземпляра класу;
  • рядки 7–10: саме у конструкторі ми будемо підраховувати створені об’єкти, оскільки створення кожного нового об’єкта викликає виконання конструктора;
  • рядок 14: зверніть увагу на запис self::$nbObjects, який вказує, що ми посилаємося на статичний атрибут класу, в якому знаходиться код, що виконується;
  • рядки 13–15: статичний метод [say] призначений для виведення кількості створених об’єктів;
  • рядки 20–22: створюються два об’єкти та виводиться лічильник об’єктів;

Результати


constructeur Classe1
constructeur Classe1
2 objets de type [Classe1] ont été construits

5.18. Видимість між батьківським і дочірнім класами

Розглянемо наступний скрипт [classes-19.php]:


<?php

class SomeParent {
  // атрибут
  private $attributeOfParent = 4;

  // метод
  public function doTest(): void {
    // хто викликає?
    print "parent :\n";
    var_dump($this);
    // батьківський елемент
    print "parent : attributeOfParent={$this->attributeOfParent}\n";
    print "parent : attributeOfChild={$this->attributeOfChild}\n";
  }

}

class SomeChild extends SomeParent {
  // атрибут
  private $attributeOfChild = 14;

  // метод
  public function doTest(): void {
    // дочірнє відображення
    print "child : attributeOfParent={$this->attributeOfParent}\n";
    print "child : attributeOfChild={$this->attributeOfChild}\n";

    // батьківський
    parent::doTest();
  }
}

// головний скрипт
print "---test1\n";
(new SomeParent())->doTest();
print "---test2\n";
(new SomeChild())->doTest();

Коментарі

  • рядки 3–17: клас [SomeParent];
  • рядки 19–32: дочірній клас [SomeChild]. Бачимо, що він успадковує клас [SomeParent] (рядок 19);
  • рядок 5: клас [SomeParent] має лише один атрибут;
  • рядки 8–17: метод [SomeParent::doTest] призначений для відображення двох атрибутів:
    • [$attributeOfParent], що належить до класу [SomeParent];
    • [$attributeOfChild], що належить до класу [SomeChild] (рядок 21);
  • рядки 10–11: відображається ідентифікатор викликаючого: метод буде викликано двома різними способами:
    • з батьківського класу [SomeParent];
    • з дочірнього класу [SomeChild];
  • рядки 13–14: виведення двох атрибутів;
  • рядки 19–32: дочірній клас [SomeChild], який успадковує клас [SomeParent] (рядок 19);
  • рядок 21: клас [SomeChild] має лише один атрибут;
  • рядок 24: метод [SomeChild::doTest] призначений для відображення двох атрибутів:
    • [$attributeOfParent], що належить до класу [SomeParent];
    • [$attributeOfChild], що належить до класу [SomeChild];
  • рядки 26–27: виведення двох атрибутів;
  • рядок 30: виклик методу [doTest] батьківського класу, який, у свою чергу, виведе обидва атрибути;
  • рядок 36: викликається метод [SomeParent::doTest];
  • рядок 38: викликається метод [SomeChild::doTest];

У першому тесті видимість обох атрибутів — [private]. Отже, можна очікувати, що дочірній клас не бачить атрибута свого батьківського класу. Для цього останній повинен мати принаймні видимість [protected]. А як щодо атрибута дочірнього класу? Чи є він видимим у батьківському класі?

Ось результати цього першого тесту:

---------------------------test1
parent :
object(SomeParent)#1 (1) {
  ["attributeOfParent":"SomeParent":private]=>
  int(4)
}
parent : attributeOfParent=4

Notice: Undefined property: SomeParent::$attributeOfChild in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-19.php on line 14
parent : attributeOfChild=
---------------------------test2

Notice: Undefined property: SomeChild::$attributeOfParent in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-19.php on line 26
child : attributeOfParent=
child : attributeOfChild=14
parent :
object(SomeChild)#1 (2) {
  ["attributeOfChild":"SomeChild":private]=>
  int(14)
  ["attributeOfParent":"SomeParent":private]=>
  int(4)
}
parent : attributeOfParent=4

Fatal error: Uncaught Error: Cannot access private property SomeChild::$attributeOfChild in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-19.php:14
Stack trace:
#0 C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-19.php(30): SomeParent->doTest()
#1 C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-19.php(39): SomeChild->doTest()
#2 {main}
  thrown in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-19.php on line 14

Коментарі

  • рядки 1–10: результати першого тесту, у якому викликається метод [SomeParent::doTest];
  • рядки 3–6: бачимо, що об’єкт, який викликає метод, має тип [SomeParent];
  • рядок 7: виведення атрибута [$attributeOfParent];
  • рядки 9–10: бачимо, що атрибут [SomeParent::$attributeOfChild] не існує. Тому він не виводиться;
  • рядки 11–30: результати другого тесту, в якому викликається метод [SomeChild::doTest];
  • рядки 13–14: бачимо, що атрибут [SomeChild::$attributeOfParent] не існує. Тому він не відображається. Це нормально: атрибут [SomeParent::$attributeOfParent] — це [private], а отже, він невідомий у дочірньому класі;
  • рядок 15: відображення атрибута [$attributeOfChild];
  • рядки 16–30: ми перебуваємо в методі [SomeParent::doTest], який викликається дочірнім класом;
  • рядки 17–22: бачимо, що [$this] має тип [SomeChild] із двома приватними атрибутами;
  • рядок 23: що дуже дивно, [$this] типу [SomeChild] бачить тут атрибут батьківського класу [$attributeOfParent];
  • рядки 25–30: що не менш дивно, [$this] типу [SomeChild] не бачить свого атрибута [$attributeOfChild];

Цей результат дуже дивний: хоча в рядках 17–21 вказано, що [$this] належить до типу [SomeChild], цей [$this] усередині методу [SomeParent::doTest] поводиться так, ніби він є екземпляром класу [SomeParent], а не класу [SomeChild].

Проведемо новий тест зі скриптом [classes-20.php]. Атрибут [$attributeOfParent] тепер має видимість [protected] (рядок 5):


<?php

class SomeParent {
  // атрибут
  protected $attributeOfParent = 4;

  // метод
  public function doTest(): void {
    // хто викликає?
    print "parent :\n";
    var_dump($this);
    // батьківський елемент
    print "parent : attributeOfParent={$this->attributeOfParent}\n";
    print "parent : attributeOfChild={$this->attributeOfChild}\n";
  }

}

class SomeChild extends SomeParent {
  // атрибут
  private $attributeOfChild = 14;

  // метод
  public function doTest(): void {
    // дочірнє відображення
    print "child : attributeOfParent={$this->attributeOfParent}\n";
    print "child : attributeOfChild={$this->attributeOfChild}\n";

    // батьківський
    parent::doTest();
  }

}

// головний скрипт
print "---------------------------test1\n";
(new SomeParent())->doTest();
print "---------------------------test2\n";
(new SomeChild())->doTest();

Результати

---------------------------test1
parent :
object(SomeParent)#1 (1) {
  ["attributeOfParent":protected]=>
  int(4)
}
parent : attributeOfParent=4

Notice: Undefined property: SomeParent::$attributeOfChild in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-20.php on line 14
parent : attributeOfChild=
---------------------------test2
child : attributeOfParent=4
child : attributeOfChild=14
parent :
object(SomeChild)#1 (2) {
  ["attributeOfChild":"SomeChild":private]=>
  int(14)
  ["attributeOfParent":protected]=>
  int(4)
}
parent : attributeOfParent=4

Fatal error: Uncaught Error: Cannot access private property SomeChild::$attributeOfChild in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-20.php:14
Stack trace:
#0 C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-20.php(30): SomeParent->doTest()
#1 C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-20.php(39): SomeChild->doTest()
#2 {main}
  thrown in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-20.php on line 14

Коментарі

  • рядок 12: клас [SomeChild] тепер бачить атрибут свого батьківського об’єкта [$attributeOfParent]. Це нормально, оскільки останній тепер має область дії [protected];
  • у методі [someParent::doTest] об’єкт [$this] має тип [SomeChild] (рядки 15–20). Він бачить атрибут свого батьківського об’єкта [$attributeOfparent] (рядок 21), але все ще не бачить власного атрибута [$attributeOfChild] (рядки 23–28);

У третьому тесті атрибут [$attributeOfChild] також має область дії [protected]:


<?php

class SomeParent {
  // атрибут
  protected $attributeOfParent = 4;

  // метод
  public function doTest(): void {
    // хто викликає?
    print "parent :\n";
    var_dump($this);
    // батьківський елемент
    print "parent : attributeOfParent={$this->attributeOfParent}\n";
    print "parent : attributeOfChild={$this->attributeOfChild}\n";
  }

}

class SomeChild extends SomeParent {
  // атрибут
  protected $attributeOfChild = 14;

  // метод
  public function doTest(): void {
    // дочірнє відображення
    print "child : attributeOfParent={$this->attributeOfParent}\n";
    print "child : attributeOfChild={$this->attributeOfChild}\n";

    // батьківський
    parent::doTest();
  }

}

// головний скрипт
print "---------------------------test1\n";
(new SomeParent())->doTest();
print "---------------------------test2\n";
(new SomeChild())->doTest();

Результати виконання такі:

---------------------------test1
parent :
object(SomeParent)#1 (1) {
  ["attributeOfParent":protected]=>
  int(4)
}
parent : attributeOfParent=4

Notice: Undefined property: SomeParent::$attributeOfChild in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-21.php on line 14
parent : attributeOfChild=
---------------------------test2
child : attributeOfParent=4
child : attributeOfChild=14
parent :
object(SomeChild)#1 (2) {
  ["attributeOfChild":protected]=>
  int(14)
  ["attributeOfParent":protected]=>
  int(4)
}
parent : attributeOfParent=4
parent : attributeOfChild=14
  • рядок 22: цього разу всередині батьківського елемента [$this] типу [SomeChild] (рядки 15–20) бачить захищений атрибут [$attributeOfChild] свого власного класу [SomeChild].

Які висновки можна зробити з цих тестів?

  • те, що екземпляр [$this] класу-батька, який використовується в методі класу-батька, бачить:
    • атрибути та методи батьківського класу незалежно від їхньої видимості;
    • не бачить атрибутів та методів своїх дочірніх класів;

Це очікувана поведінка.

  • що екземпляр [$this] класу-дочірнього, який використовується в методі класу-дочірнього, бачить:
    • атрибути та методи батьківського класу, якщо вони мають принаймні рівень видимості [protected]. Ті, що мають рівень видимості [private], не бачаться;
    • атрибути та методи дочірнього класу незалежно від їхньої видимості;

Це очікувана поведінка.

  • Інстанція дочірнього класу [$this], яка використовується в методі батьківського класу, бачить:
    • атрибути та методи батьківського класу незалежно від їхньої видимості;
    • атрибути та методи власного класу — лише якщо вони мають принаймні рівень видимості [protected]. Ті, що мають рівень видимості [private], не відображаються;

Це несподівана поведінка.

5.19. Кодування класу jSON

У класі часто зустрічається метод [__toString]: він повинен повертати рядок, що представляє об’єкт, який його викликає. Може виникнути спокуса зробити цей рядок рядком типу jSON. Зараз ми розглянемо цей варіант.

Image

Ми будемо використовувати такий клас [Personne]:


<?php

class Personne {
  // атрибути
  private $nom;
  private $prénom;
  private $âge;
  private $enfants;

  // глобальний сеттер
  public function setFromArray(array $arrayOfAttributes): Personne {
    // ініціалізація деяких атрибутів класу
    foreach ($arrayOfAttributes as $attribute => $value) {
      $this->$attribute = $value;
    }
    // повертається об’єкт
    return $this;
  }

  // геттери
  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 {
    // ідентифікація об’єкта
    var_dump($this);
    // отримуємо його атрибути
    $attributes = \get_object_vars($this);
    var_dump($attributes);
    // повертаємо рядок jSON з атрибутами
    return \json_encode($attributes, JSON_UNESCAPED_UNICODE);
  }

}

Коментарі

  • рядки 5–8: чотири атрибути класу;
  • рядки 20–35: методи-гетери, що дозволяють отримати значення цих атрибутів;
  • рядки 11–18: глобальний сеттер, який дозволяє ініціалізувати атрибути на основі асоціативного масиву [$arrayOfAttributes], ключі якого збігаються з атрибутами класу;
  • рядки 38–46: метод [__toString] класу;
  • рядок 42: функція PHP [get_object_vars] дозволяє отримати значення атрибутів класу у вигляді асоціативного масиву [‘nom’=>’nom1’, ‘prénom’=>’prénom1’, ‘âge’=>’âge1’, ‘enfants’=>[]];
  • рядок 45: повертається рядок jSON з цього масиву атрибутів;

Розглянемо скрипт [json-01.php], який використовує клас [Personne]:


<?php

// клас «Людина»
require "Personne.php";

// створення екземпляра батька
$père = new Personne();
// ініціалізація батька
$père->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Dieudonné",
  "âge" => 58
]);
// створення екземпляра та ініціалізація «child1»
$enfant1 = (new Personne())->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Sylvain",
  "âge" => 17
  ]);
// створення екземпляра та ініціалізація дочірнього об’єкта 2
$enfant2 = (new Personne())->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Géraldine",
  "âge" => 12
  ]);
// ініціалізація дочірніх об’єктів батьківського об’єкта
$père->setFromArray([
  "enfants" => [$enfant1, $enfant2]
]);

// виведення елементів батьківського об'єкта
$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";

Коментарі

  • рядки 6–13: ініціалізуємо об’єкт [Personne] [$père] за допомогою методу [Personne::setFromArray], який дозволяєініціалізувати об’єкт [Personne] за допомогою масиву, ключі якого збігаються з атрибутами класу [Personne];
  • рядки 14–19: об’єкт [Personne] [$enfant1] ініціалізується таким самим чином;
  • рядки 21–25: ініціалізується об’єкт [Personne] [$enfant2];
  • рядки 27–29: ініціалізується атрибут [$père→enfants] масивом із двох дочірніх елементів;
  • рядки 32–33: обидва дочірні елементи привласнюються батьківському елементу;
  • рядок 35: операція [print] намагатиметься перетворити об’єкт [$enfant1] на рядок символів. Для цього вона використовує метод [__toString] цього об’єкта. Отже, ми сподіваємося побачити рядок jSON цього об’єкта;
  • рядки 38–39: те саме робимо з батьківським об’єктом;

Результати такі:


------------------------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":[{},{}]}

Коментарі

  • рядки 2–11: об’єкт [$enfant1];
  • рядки 12–21: масив атрибутів об’єкта [$enfant1]. Усі вони присутні;
  • рядок 22: ми маємо рядок jSON об’єкта [$enfant1];
  • рядки 23–44: те саме для об’єкта [$enfant2];
  • рядки 45–112: для батька ситуація дещо інша, оскільки його атрибут [enfants] не є NULL, як це було у дітей;
  • рядок 112: бачимо, що в рядку jSON батька відсутні діти;
  • рядки 79–111: бачимо, що в таблиці атрибутів батька дитина 1 (рядки 89–98) залишилася об’єктом, так само як і дитина 2 (рядки 99–110). Іншими словами, вираз [\get_object_vars($this)], де [$this] позначає батька, не є рекурсивним: якщо атрибут класу [Personne] сам є об’єктом, вираз [\get_object_vars($this)] не намагається отримати його масив атрибутів;

Можна це покращити. Змінимо клас [Personne] на такий клас [Personne2]:


<?php

class Personne2 {
  // атрибути
  private $nom;
  private $prénom;
  private $âge;
  private $enfants;

  // глобальний сеттер
  public function setFromArray(array $arrayOfAttributes): Personne2 {

    // повертається об’єкт
    return $this;
  }

  // геттери
  public function getNom() {
    return $this->nom;
  }



  // __toString
  public function __toString(): string {
    // отримуємо атрибути об’єкта
    $attributes = $this->getAttributes($this);
    $enfants = $attributes["enfants"];
    if ($enfants != NULL) {
      $attributes["enfants"] = [$enfants[0]->getAttributes(), $enfants[1]->getAttributes()];
    }
    // повертає рядок JSON з атрибутами
    return \json_encode($attributes, JSON_UNESCAPED_UNICODE);
  }

  public function getAttributes(): array {
    return \get_object_vars($this);
  }

}

Коментарі

  • рядки 36–38: функція [getAttributes] повертає масив атрибутів об’єкта, що її викликає;
  • рядки 25–34: функція [__toString];
  • рядок 27: атрибути класу [Personne] зберігаються у масиві [$attributes];
  • рядок 28: з попереднього прикладу відомо, що [$attributes["enfants"]] є масивом із двох об’єктів типу [Personne];
  • рядки 29–31: обидва об’єкти замінюються на їхній масив атрибутів;
  • рядок 33: залишається лише закодувати сформований масив атрибутів у форматі jSON;

Скрипт [json-02.php] використовує клас [Personne2] наступним чином:


<?php

// клас Personne2
require "Personne2.php";

// створення екземпляра батьківського класу
$père = new Personne2();
// ініціалізація
$père->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Dieudonné",
  "âge" => 58
]);
// створення екземпляра та ініціалізація «child1»
$enfant1 = (new Personne2())->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Sylvain",
  "âge" => 17
  ]);
// створення екземпляра та ініціалізація «дочка2»
$enfant2 = (new Personne2())->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Géraldine",
  "âge" => 12
  ]);
// ініціалізація дочірніх об’єктів батьківського класу
$père->setFromArray([
  "enfants" => [$enfant1, $enfant2]
]);

// виведення на екран батьківського об'єкта
print "------------------------père\n";
print "père=$père\n";

Скрипт [json-02.php] ідентичний скрипту [json-01.php], за винятком того, що клас [Personne2] замінив клас [Personne].

Результати виконання такі:

------------------------père
père={"nom":"Bertholomé","prénom":"Dieudonné","âge":58,"enfants":[{"nom":"Bertholomé","prénom":"Sylvain","âge":17,"enfants":null},{"nom":"Bertholomé","prénom":"Géraldine","âge":12,"enfants":null}]}

Цього разу ми дійсно отримали дітей разом із батьком.

Попереднє рішення є незадовільним, оскільки діти можуть мати власних дітей. Тоді ми знову стикаємося з попередньою проблемою.

Клас [Personne3] вирішує цю проблему таким чином:


<?php

class Personne3 {
  // атрибути
  private $nom;
  private $prénom;
  private $âge;
  private $enfants;

  // глобальний сеттер
  public function setFromArray(array $arrayOfAttributes): Personne3 {

  }

  // геттери


  // __toString
  public function __toString(): string {
    // повертає рядок JSON атрибутів
    $attributes = [];
    $this->getRecursiveAttributes($attributes, $this, []);
    // рядок JSON атрибутів
    return \json_encode($attributes, JSON_UNESCAPED_UNICODE);
  }

  public function getAttributes(): array {
    return \get_object_vars($this);
  }

  private function getRecursiveAttributes(array &$attributes, $value, $keys): void {
    // аналіз значення [$value]
    // $keys — це масив [key1, key2, .., keyn]
    // $value=$attributesQZXW2HTMLBW2tleTFdZQXQZXW2HTMLBW2tleTJdZQX….[keyn]
    // якщо [$value] є об’єктом, то використовується його метод [getAttributes]
    if (\is_object($value)) {
      // атрибути об’єкта [$value]
      $objectAttributes = $value->getAttributes();
      // що робити з результатом?
      if ($keys) {
        // у [$attributes] ми замінимо $value на масив його атрибутів
        // потрібно побудувати елемент $attributesQZXW2HTMLBW2tleTFdZQXQZXW2HTMLBW2tleTJdZQX…[keyn]
        //, де $keys — це масив [key1, key2, .., keyn]
        // беремо посилання на масив [$attributes]
        $attribute = &$attributes;
        // сканується масив ключів
        foreach ($keys as $key) {
          // беремо посилання на атрибут
          $attribute = &$attribute[$key];
        }
        // тут $attribut та $attributesQZXW2HTMLBW2tleTFdZQXQZXW2HTMLBW2tleTJdZQX…[key(n)] є ідентичними
        // вони мають однакове місце в пам’яті
        // об’єкт [$value] замінюється на його масив атрибутів;
        // необхідно записати $attributesQZXW2HTMLBW2tleTFdZQXQZXW2HTMLBW2tleTJdZQX…[keyn]=$objectAttributes
        // що еквівалентно $attribute = $objectAttributes
        $attribute = $objectAttributes;
      } else {
        // ключів немає — ми перебуваємо на початку дослідження об’єкта
        // $objectAttributes представляє атрибути першого рівня класу
        $attributes += $objectAttributes;
      }
      // можливо, у [$objectAttributes] ще є об’єкти
      // ми досліджуємо атрибути [$objectAttributes]
      $this->getRecursiveAttributes($attributes, $objectAttributes, $keys);
    } else {
      if (\is_array($value)) {
        // маємо масив — аналізуємо кожен з його елементів
        foreach ($value as $key => $élément) {
          // додаємо поточний ключ до масиву $keys
          \array_push($keys, $key);
          // аналізуємо $élément
          $this->getRecursiveAttributes($attributes, $élément, $keys);
          // видаляємо з масиву $keys ключ, який щойно проаналізували
          \array_pop($keys);
        }
      }
    }
  }

Коментарі

  • рядки 21–22: цього разу метод [__toString] запитує атрибути свого класу і вимагає, щоб це було зроблено рекурсивно: якщо атрибут є об’єктом або масивом об’єктів, то кожен об’єкт має бути замінений своїм масивом атрибутів у кінцевому масиві атрибутів класу;
  • рядки 31–78: функція [getRecursiveAttributes] виконує цю роботу. Ми прокоментували її код. Написання рекурсивної функції часто є складним завданням. Так само й у цьому випадку. Читач нічого не втратить, якщо не зрозуміє цього. Існують бібліотеки, які виконують цю роботу. Рекурсивний виклик відбувається у рядках 64 та 72;
  • цікавість цього коду полягає в тому, що він був написаний не лише для класу [Personne3]. Він підходить для будь-якого класу, атрибути якого мають значення різних типів об’єктів, за умови, що класи, які використовує головний клас, мають, як і він, метод [getAttributes] у рядках 27–29

Скрипт [json-03.php] використовує клас [Personne3] наступним чином:


<?php

// клас Personne3
require "Personne3.php";

// створення екземпляра батьківського класу
$père = new Personne3();
// ініціалізація
$père->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Dieudonné",
  "âge" => 58
]);
// створення екземпляра та ініціалізація «дитина1»
$enfant1 = (new Personne3())->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Sylvain",
  "âge" => 27
  ]);
// створення екземпляра та ініціалізація «дочка2»
$enfant2 = (new Personne3())->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Géraldine",
  "âge" => 12
  ]);
// ініціалізація дочірніх об’єктів батьківського об’єкта
$père->setFromArray([
  "enfants" => [$enfant1, $enfant2]
]);
// створення екземпляра та ініціалізація дочірнього об’єкта 11
$enfant11 = (new Personne3())->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Gaëtan",
  "âge" => 2
  ]);
// створення екземпляра та ініціалізація дочірнього об’єкта 12
$enfant12 = (new Personne3())->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Mathilde",
  "âge" => 1
  ]);
// ініціалізація дочірніх об’єктів об’єкта «дочірній об’єкт 1»
$enfant1->setFromArray([
  "enfants" => [$enfant11, $enfant12]
]);
// виведення на екран батьківського об'єкта
print "------------------------père\n";
print "père=$père\n";
  • рядки 30–45: класу [$enfant1] передаються два дочірні елементи;

Результати виконання такі:

------------------------père
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}]}

Якщо оформити цей результат, отримаємо таке:


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
        }
    ]
}

Ми успішно витягли ланцюжки jSON з усіх об’єктів [Personne], що утворюють батьківський об’єкт.