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

Трейты

Родитель у класса только один. Но бывает, что одно и то же умение нужно классам из разных семейств: роботу, принтеру и гостю сайта, у которых нет общего родителя. Копировать один метод в три класса не хочется, а общий родитель для них выглядел бы странно.

Вернёмся к чертежам. Представьте готовый модуль — например, балкон, который завод выпускает отдельно. Его прикручивают к домам из любых семейств чертежей, и у каждого дома появляется свой балкон.

В PHP такой модуль называется трейтом: это набор методов, который подключают к классу словом use.

Как подключить трейт

Трейт объявляют словом trait, а внутри класса подключают через use:

PHP
<?php

trait Greets
{
    public function hello(): string
    {
        return 'hello from ' . $this::class;
    }
}

class Robot
{
    use Greets;
}
echo (new Robot())->hello(), "\n"; // hello from Robot

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

  1. trait Greets объявляет трейт с одним методом hello(). Сам по себе он ничего не делает.
  2. use Greets; внутри Robot подключает трейт к классу.
  3. PHP копирует методы трейта в класс, как будто вы набрали их в Robot руками.
  4. Поэтому $this::class внутри hello() даёт Robot: метод уже стал методом класса, а не трейта.

Тот же use Greets; можно написать в любом другом классе, и у каждого появится свой hello(). Такое переиспользование называют горизонтальным: код делится между классами из разных семейств, а не передаётся сверху вниз по наследованию.

В трейте можно объявлять и свойства — они тоже копируются в каждый класс. А вот объект из трейта не создать: new Greets() даёт Error: Cannot instantiate trait Greets.

Чей метод победит

Что будет, если метод с тем же именем есть и у родителя класса, и в трейте? Давайте посмотрим:

PHP
class Machine
{
    public function hello(): string
    {
        return 'beep';
    }
}

class Printer extends Machine
{
    use Greets;
}

echo (new Printer())->hello(), "\n"; // hello from Printer

Метод трейта оказался сильнее метода родителя: Printer получил hello() из Greets, и версия Machine не сработала. А если метод с тем же именем объявлен в самом классе, побеждает он:

PHP
class Guest
{
    use Greets;

    public function hello(): string
    {
        return 'hi, I am a guest';
    }
}

echo (new Guest())->hello(), "\n"; // hi, I am a guest

Порядок, в котором PHP выбирает метод:

  1. метод, объявленный в самом классе;
  2. метод из трейта;
  3. метод, унаследованный от родителя.

Одинаковое имя у метода класса и метода трейта — не ошибка: PHP молча оставляет метод класса. Ошибка бывает, когда два трейта приносят в один класс методы с одним именем. Тогда PHP остановит скрипт с фатальной ошибкой на объявлении класса, а победителя выбирают словом insteadof в фигурных скобках после use.

Зачем нужны трейты при одиночном наследовании — частый вопрос на собеседовании. Ответ: чтобы классы без общего родителя делили готовый код.

Трейт — не тип

Здесь картинка с балконом неточна. По дому видно, что к нему прикручен балкон, а проверка типа не замечает у объекта трейта.

Методы трейта становятся методами класса, а сам трейт типом не становится. Поэтому $robot instanceof Greets даёт false, а в типе параметра трейт не работает.

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

Трейт указали как тип параметра функции:

PHP
function welcome(Greets $g): string
{
    return $g->hello();
}

echo welcome(new Robot()), "\n"; // ✗
TEXT
Fatal error: Uncaught TypeError: welcome(): Argument #1 ($g) must be of type Greets, Robot given

Для PHP Greets в типе параметра — имя класса или интерфейса, а объект Robot не относится ни к классу, ни к интерфейсу с таким именем: трейт не входит в его типы. Если нужен тип, заведите интерфейс с методом hello() и подпишите на него класс: class Robot implements CanGreet { use Greets; }. Трейт принесёт метод, а интерфейс даст тип для параметра.

Главное

  • Трейт — набор методов и свойств, который use копирует в класс.
  • Трейты дают общий код классам без общего родителя — это горизонтальное переиспользование.
  • Метод самого класса сильнее метода трейта, а метод трейта сильнее метода родителя.
  • Трейт — не тип: instanceof его не видит, и в типе параметра он не работает.