Трейты
Родитель у класса только один. Но бывает, что одно и то же умение нужно классам из разных семейств: роботу, принтеру и гостю сайта, у которых нет общего родителя. Копировать один метод в три класса не хочется, а общий родитель для них выглядел бы странно.
Вернёмся к чертежам. Представьте готовый модуль — например, балкон, который завод выпускает отдельно. Его прикручивают к домам из любых семейств чертежей, и у каждого дома появляется свой балкон.
В PHP такой модуль называется трейтом: это набор методов, который подключают к классу словом use.
Как подключить трейт
Трейт объявляют словом trait, а внутри класса подключают через use:
<?php
trait Greets
{
public function hello(): string
{
return 'hello from ' . $this::class;
}
}
class Robot
{
use Greets;
}
echo (new Robot())->hello(), "\n"; // hello from RobotДавайте разберём по шагам:
trait Greetsобъявляет трейт с одним методомhello(). Сам по себе он ничего не делает.use Greets;внутриRobotподключает трейт к классу.- PHP копирует методы трейта в класс, как будто вы набрали их в
Robotруками. - Поэтому
$this::classвнутриhello()даётRobot: метод уже стал методом класса, а не трейта.
Тот же use Greets; можно написать в любом другом классе, и у каждого появится свой hello(). Такое переиспользование называют горизонтальным: код делится между классами из разных семейств, а не передаётся сверху вниз по наследованию.
В трейте можно объявлять и свойства — они тоже копируются в каждый класс. А вот объект из трейта не создать: new Greets() даёт Error: Cannot instantiate trait Greets.
Чей метод победит
Что будет, если метод с тем же именем есть и у родителя класса, и в трейте? Давайте посмотрим:
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 не сработала. А если метод с тем же именем объявлен в самом классе, побеждает он:
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 выбирает метод:
- метод, объявленный в самом классе;
- метод из трейта;
- метод, унаследованный от родителя.
Одинаковое имя у метода класса и метода трейта — не ошибка: PHP молча оставляет метод класса. Ошибка бывает, когда два трейта приносят в один класс методы с одним именем. Тогда PHP остановит скрипт с фатальной ошибкой на объявлении класса, а победителя выбирают словом insteadof в фигурных скобках после use.
Зачем нужны трейты при одиночном наследовании — частый вопрос на собеседовании. Ответ: чтобы классы без общего родителя делили готовый код.
Трейт — не тип
Здесь картинка с балконом неточна. По дому видно, что к нему прикручен балкон, а проверка типа не замечает у объекта трейта.
Методы трейта становятся методами класса, а сам трейт типом не становится. Поэтому $robot instanceof Greets даёт false, а в типе параметра трейт не работает.
Частая ошибка
Трейт указали как тип параметра функции:
function welcome(Greets $g): string
{
return $g->hello();
}
echo welcome(new Robot()), "\n"; // ✗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его не видит, и в типе параметра он не работает.