Наследование и parent::
Чему вы научитесь:
- наследовать класс через
extends, переопределять его методы и вызывать версию родителя черезparent::; - описывать общий контракт абстрактным классом или интерфейсом и проверять его через
instanceof; - подключать готовые методы трейтом и знать, чей метод побеждает при совпадении имён;
- заводить перечисления
enumи получать вариант из строки черезfromиtryFrom.

В прошлой теме класс был чертежом дома, а объекты — домами, построенными по нему. Теперь представьте целое семейство чертежей. Архитектор не рисует заново дом с другой кухней: он берёт готовый чертёж и пишет «всё как в основном, только кухня другая».
Новый чертёж хранит только отличия, а остальное получает от основного. Если основной чертёж исправят, исправление сразу появится и в новом. Ради этого в PHP есть наследование: класс получает все свойства и методы другого класса и может поменять часть из них.
Без наследования пришлось бы копировать одинаковые методы в каждый похожий класс, а потом исправлять одну и ту же ошибку в каждой копии.
extends: всё от родителя
Класс, от которого наследуют, называют родительским, а класс-наследник — дочерним. Связь между ними записывают словом extends:
<?php
class Animal
{
public function __construct(protected string $name) {}
public function speak(): string
{
return $this->name . ': hum';
}
}
class Cat extends Animal {}
echo (new Cat('Tom'))->speak(), "\n"; // Tom: humДавайте разберём по шагам:
Animal— обычный класс: у него конструктор с продвижением свойства$nameи методspeak().class Cat extends Animal {}объявляет дочерний класс с пустым телом. Своего кода уCatнет, но он уже умеет всё, что умеетAnimal.new Cat('Tom')вызывает конструктор, которыйCatполучил от родителя, и в$nameпопадает'Tom'.(new Cat('Tom'))->speak()создаёт объект и сразу вызывает у него метод. ВCatметодаspeak()нет, поэтому PHP берёт его уAnimal.
У класса в PHP только один родитель: запись class Cat extends Animal, Pet — ошибка разбора. Зато у родителя может быть сколько угодно дочерних классов, а у них — свои.
Переопределение и parent::
Дочерний класс может объявить метод с тем же именем, что у родителя. Это называется переопределением: у объектов дочернего класса работает новая версия. Версия родителя при этом не пропадает, до неё можно добраться через parent:::
class Dog extends Animal
{
public function speak(): string
{
return $this->name . ': woof';
}
public function greet(): string
{
return parent::speak() . ', ' . $this->speak();
}
}
echo (new Dog('Rex'))->greet(), "\n"; // Rex: hum, Rex: woofРазберём, что здесь происходит:
Dogпереопределяетspeak(): собака отвечаетwoof, а неhum.- В
greet()вызовparent::speak()берёт версию изAnimalи получает'Rex: hum'. - Вызов
$this->speak()в том же методе берёт версию самогоDogи получает'Rex: woof'.
Переопределяйте метод с теми же параметрами и тем же типом результата, что у родителя. А метод с пометкой final переопределить нельзя: такой файл PHP не запустит и сообщит о фатальной ошибке.
Метод выбирается по объекту
Пусть метод talk() написан в Animal и внутри вызывает $this->speak(). Какой speak() сработает, если объект — Dog?
В $this лежит объект, и PHP смотрит на его класс, а не на класс, где написан код. Поэтому для объекта Dog код из Animal попадёт в Dog::speak. Нажимайте «Следующий шаг» и следите, кто лежит в $this и какой speak() открывается.
1<?php2 3class Animal4{5 public function speak(): string6 {7 return 'hum';8 }9 public function talk(): string10 {11 return $this->speak() . '!';12 }13}14class Dog extends Animal15{16 public function speak(): string17 {18 return 'woof';19 }20}21 22$a = new Animal();23$d = new Dog();24echo $a->talk(), "\n";25echo $d->talk(), "\n";
Обычный объект Animal: в нём нет ничего от Dog.
Правило, которое показывает виджет:
$thisвнутри метода — это объект, у которого метод вызвали, даже если сам метод написан в родителе;$this->speak()ищет метод в классе этого объекта и только потом у его родителей.
Здесь картинка с чертежами неточна. В настоящем доме комната по основному чертежу ничего не знает о новом. А в PHP метод talk() из Animal спрашивает сам объект, как ему говорить, и объект Dog отвечает своей версией.
Такое поведение называют полиморфизмом: один вызов работает по-разному в зависимости от класса объекта. На собеседовании часто спрашивают: «Что вызовет $this->method() в коде родителя, если объект — наследник?» Ответ: версию из класса объекта.
Конструктор дочернего класса
Если у дочернего класса есть свой конструктор, он целиком заменяет родительский. Сам PHP конструктор родителя не вызывает, поэтому дочерний класс зовёт его явно, через parent::__construct():
class Parrot extends Animal
{
public function __construct(string $name, private string $word)
{
parent::__construct($name);
}
public function speak(): string
{
return $this->name . ': ' . $this->word;
}
}
echo (new Parrot('Kesha', 'hello'))->speak(), "\n"; // Kesha: hello- Конструктор
Parrotпринимает имя и слово. Слово он сохраняет в своём свойстве$word. - Имя он передаёт в
parent::__construct($name), и конструкторAnimalзаписывает его в$name. speak()читает оба свойства и возвращает'Kesha: hello'.
protected и private
В Animal свойство $name объявлено как protected, поэтому методы Dog и Parrot могут его читать. Три уровня видимости при наследовании:
public— виден везде: в самом классе, в дочерних классах и снаружи;protected— виден в самом классе и во всех его дочерних классах, но не снаружи;private— виден только в классе, который его объявил.
Если бы $name был private, чтение $this->name в методе Dog дало бы предупреждение, а вместо имени — null:
Warning: Undefined property: Dog::$nameЕсли у родителя свойство private, дочерний класс может объявить своё свойство с тем же именем: это будет другое свойство. Разницу между private и protected при наследовании тоже часто спрашивают на собеседовании.
Частая ошибка
В дочернем классе написали свой конструктор и забыли вызвать конструктор родителя:
class Hamster extends Animal
{
public function __construct(private int $age) {}
}
echo (new Hamster(1))->speak(), "\n"; // ✗Fatal error: Uncaught Error: Typed property Animal::$name must not be accessed before initializationКонструктор Hamster заменил конструктор Animal, и значение в $name никто не записал. Метод speak() читает свойство, которому ещё ничего не присвоили, и PHP останавливает скрипт. Исправление — принять имя в конструкторе и передать его дальше через parent::__construct($name), как в Parrot.
Главное
class Dog extends Animalдаёт дочернему классу все свойства и методы родителя; родитель у класса один.- Переопределённый метод работает у объектов дочернего класса, а версия родителя доступна через
parent::. $this->method()в коде родителя вызывает версию из класса объекта — это полиморфизм.- Дочерний класс со своим конструктором вызывает
parent::__construct()явно;protectedвиден в дочерних классах,private— нет.