Перейти к содержимому
Теория · Наследование и интерфейсы · урок 1 из 8

Наследование и parent::

Чему вы научитесь

Чему вы научитесь:

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

В прошлой теме класс был чертежом дома, а объекты — домами, построенными по нему. Теперь представьте целое семейство чертежей. Архитектор не рисует заново дом с другой кухней: он берёт готовый чертёж и пишет «всё как в основном, только кухня другая».

Новый чертёж хранит только отличия, а остальное получает от основного. Если основной чертёж исправят, исправление сразу появится и в новом. Ради этого в PHP есть наследование: класс получает все свойства и методы другого класса и может поменять часть из них.

Без наследования пришлось бы копировать одинаковые методы в каждый похожий класс, а потом исправлять одну и ту же ошибку в каждой копии.

extends: всё от родителя

Класс, от которого наследуют, называют родительским, а класс-наследник — дочерним. Связь между ними записывают словом extends:

PHP
<?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

Давайте разберём по шагам:

  1. Animal — обычный класс: у него конструктор с продвижением свойства $name и метод speak().
  2. class Cat extends Animal {} объявляет дочерний класс с пустым телом. Своего кода у Cat нет, но он уже умеет всё, что умеет Animal.
  3. new Cat('Tom') вызывает конструктор, который Cat получил от родителя, и в $name попадает 'Tom'.
  4. (new Cat('Tom'))->speak() создаёт объект и сразу вызывает у него метод. В Cat метода speak() нет, поэтому PHP берёт его у Animal.

У класса в PHP только один родитель: запись class Cat extends Animal, Pet — ошибка разбора. Зато у родителя может быть сколько угодно дочерних классов, а у них — свои.

Переопределение и parent::

Дочерний класс может объявить метод с тем же именем, что у родителя. Это называется переопределением: у объектов дочернего класса работает новая версия. Версия родителя при этом не пропадает, до неё можно добраться через parent:::

PHP
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

Разберём, что здесь происходит:

  1. Dog переопределяет speak(): собака отвечает woof, а не hum.
  2. В greet() вызов parent::speak() берёт версию из Animal и получает 'Rex: hum'.
  3. Вызов $this->speak() в том же методе берёт версию самого Dog и получает 'Rex: woof'.

Переопределяйте метод с теми же параметрами и тем же типом результата, что у родителя. А метод с пометкой final переопределить нельзя: такой файл PHP не запустит и сообщит о фатальной ошибке.

Метод выбирается по объекту

Пусть метод talk() написан в Animal и внутри вызывает $this->speak(). Какой speak() сработает, если объект — Dog?

В $this лежит объект, и PHP смотрит на его класс, а не на класс, где написан код. Поэтому для объекта Dog код из Animal попадёт в Dog::speak. Нажимайте «Следующий шаг» и следите, кто лежит в $this и какой speak() открывается.

ВживуюЧей speak вызовет talkшаг 1 из 10 · рамок: 1

Обычный объект Animal: в нём нет ничего от Dog.

Следите, кто лежит в $this и какой speak открывается

Правило, которое показывает виджет:

  • $this внутри метода — это объект, у которого метод вызвали, даже если сам метод написан в родителе;
  • $this->speak() ищет метод в классе этого объекта и только потом у его родителей.

Здесь картинка с чертежами неточна. В настоящем доме комната по основному чертежу ничего не знает о новом. А в PHP метод talk() из Animal спрашивает сам объект, как ему говорить, и объект Dog отвечает своей версией.

Такое поведение называют полиморфизмом: один вызов работает по-разному в зависимости от класса объекта. На собеседовании часто спрашивают: «Что вызовет $this->method() в коде родителя, если объект — наследник?» Ответ: версию из класса объекта.

Конструктор дочернего класса

Если у дочернего класса есть свой конструктор, он целиком заменяет родительский. Сам PHP конструктор родителя не вызывает, поэтому дочерний класс зовёт его явно, через parent::__construct():

PHP
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
  1. Конструктор Parrot принимает имя и слово. Слово он сохраняет в своём свойстве $word.
  2. Имя он передаёт в parent::__construct($name), и конструктор Animal записывает его в $name.
  3. speak() читает оба свойства и возвращает 'Kesha: hello'.

protected и private

В Animal свойство $name объявлено как protected, поэтому методы Dog и Parrot могут его читать. Три уровня видимости при наследовании:

  • public — виден везде: в самом классе, в дочерних классах и снаружи;
  • protected — виден в самом классе и во всех его дочерних классах, но не снаружи;
  • private — виден только в классе, который его объявил.

Если бы $name был private, чтение $this->name в методе Dog дало бы предупреждение, а вместо имени — null:

TEXT
Warning: Undefined property: Dog::$name

Если у родителя свойство private, дочерний класс может объявить своё свойство с тем же именем: это будет другое свойство. Разницу между private и protected при наследовании тоже часто спрашивают на собеседовании.

Частая ошибка

В дочернем классе написали свой конструктор и забыли вызвать конструктор родителя:

PHP
class Hamster extends Animal
{
    public function __construct(private int $age) {}
}

echo (new Hamster(1))->speak(), "\n"; // ✗
TEXT
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 — нет.