Трейты

Способ, которым в языке PHP один и тот же код внедряют в несвязанные иерархии классов, называется трейтами (англ. Traits).

Трейты — механизм, который разрешает повторно использовать код в языках с одиночным наследованием наподобие PHP. Задача трейта — уменьшить ограничения одиночного наследования, разрешая разработчику легко переиспользовать наборы методов в нескольких независимых классах, которые находятся в разных иерархиях наследования. Семантику комбинации трейтов и классов определили так, чтобы снизить уровень сложности и избежать типичных проблем, свойственных множественному наследованию и примесям (англ. Mixins).

Трейт похож на класс, но предназначается только для группировки функциональности тонко контролируемым и согласованным образом. Нельзя создать отдельный экземпляр трейта. Трейт дополняет традиционное наследование и разрешает выстраивать горизонтальную композицию поведения; другими словами, трейт играет роль приложения к членам класса, которое не требует наследования.

Пример #1 Пример трейта

<?php

trait TraitA {
    public function sayHello()
    {
        echo 'Hello';
    }
}

trait TraitB {
    public function sayWorld()
    {
        echo 'World';
    }
}

class MyHelloWorld
{
    use TraitA, TraitB; // Класс разрешает внедрять несколько трейтов

    public function sayHelloWorld()
    {
        $this->sayHello();
        echo ' ';

        $this->sayWorld();
        echo "!\n";
    }
}

$myHelloWorld = new MyHelloWorld();
$myHelloWorld->sayHelloWorld();

?>

Результат выполнения приведённого примера:

Hello World!

Приоритет

Член, который класс унаследовал из базового класса, переопределяется членом, который внедрился трейтом. Порядок приоритета выстраивается так, что методы трейта переопределяются методами текущего класса, а методы, которые класс унаследовал из базового класса, переопределяются методами трейта.

Пример #2 Пример того, в каком порядке выстраивается приоритет

Метод, который класс унаследовал из базового класса, переопределяется методом, который внедрился в класс MyHelloWorld из трейта SayWorld. Методы трейта ведут себя как методы класса MyHelloWorld. Порядок приоритета такой: методами текущего класса переопределяются методы трейта, которыми переопределяются методы базового класса.

<?php

class Base
{
    public function sayHello()
    {
        echo 'Hello ';
    }
}

trait SayWorld
{
    public function sayHello()
    {
        parent::sayHello();
        echo 'World!';
    }
}

class MyHelloWorld extends Base
{
    use SayWorld;
}

$o = new MyHelloWorld();
$o->sayHello();

?>

Результат выполнения приведённого примера:

Hello World!

Пример #3 Пример альтернативного порядка приоритета

<?php

trait HelloWorld
{
    public function sayHello()
    {
        echo 'Hello World!';
    }
}

class TheWorldIsNotEnough
{
    use HelloWorld;

    public function sayHello()
    {
        echo 'Hello Universe!';
    }
}

$o = new TheWorldIsNotEnough();
$o->sayHello();

?>

Результат выполнения приведённого примера:

Hello Universe!

Несколько трейтов

Названия трейтов перечисляют через запятую в инструкции use, чтобы добавить в класс несколько трейтов.

Пример #4 Пример внедрения нескольких трейтов

<?php

trait Hello
{
    public function sayHello()
    {
        echo 'Hello ';
    }
}

trait World
{
    public function sayWorld()
    {
        echo 'World';
    }
}

class MyHelloWorld
{
    use Hello, World;

    public function sayExclamationMark()
    {
        echo '!';
    }
}

$o = new MyHelloWorld();
$o->sayHello();
$o->sayWorld();
$o->sayExclamationMark();

?>

Результат выполнения приведённого примера:

Hello World!

Разрешение конфликтов

При добавлении двумя трейтами метода с одним и тем же названием возникает фатальная ошибка, если конфликт явно не разрешили.

Оператор insteadof разрешает конфликты имён и указывает, метод какого трейта исключить из класса, когда название метода одного трейта совпадает с названием метода другого трейта, который включили в этот же класс.

Для добавления псевдонима методу трейта указывают оператор as, поскольку предыдущий оператор только исключает методы. Обратите внимание, оператор as не переименовывает метод и не влияет ни на какие другие методы.

Пример #5 Пример разрешения конфликтов

В этом примере в класс Talker включили трейты A и B. Поскольку в трейтах A и B содержатся методы, которые вступают в конфликт, класс использует вариант метода smallTalk из трейта B, а вариант метода bigTalk из трейта A.

Оператор as в классе Aliased_Talker разрешает вызывать метод bigTalk трейта B по псевдониму talk.

<?php

trait A
{
    public function smallTalk()
    {
        echo 'a';
    }

    public function bigTalk()
    {
        echo 'A';
    }
}

trait B
{
    public function smallTalk()
    {
        echo 'b';
    }

    public function bigTalk()
    {
        echo 'B';
    }
}

class Talker
{
    use A, B {
        B::smallTalk insteadof A;
        A::bigTalk insteadof B;
    }
}

class Aliased_Talker
{
    use A, B {
        B::smallTalk insteadof A;
        A::bigTalk insteadof B;
        B::bigTalk as talk;
    }
}

?>

Изменение видимости метода

Синтаксис с оператором as умеет также настраивать видимость метода в классе.

Пример #6 Пример изменения видимости метода

<?php

trait HelloWorld
{
    public function sayHello()
    {
        echo 'Привет, мир!';
    }
}

// Изменим видимость метода sayHello
class MyClass1
{
    use HelloWorld {
        sayHello as protected;
    }
}

// Создадим псевдоним метода и изменим видимость этого метода.
// Видимость метода sayHello не изменилась
class MyClass2
{
    use HelloWorld {
        sayHello as private myPrivateHello;
    }
}

?>

Импорт трейтов в трейты

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

Пример #7 Пример трейтов, которые состоят из трейтов

<?php

trait Hello
{
    public function sayHello()
    {
        echo 'Hello ';
    }
}

trait World
{
    public function sayWorld()
    {
        echo 'World!';
    }
}

trait HelloWorld
{
    use Hello, World;
}

class MyHelloWorld
{
    use HelloWorld;
}

$o = new MyHelloWorld();
$o->sayHello();
$o->sayWorld();

?>

Результат выполнения приведённого примера:

Hello World!

Абстрактные члены трейтов

Трейты поддерживают абстрактные методы, чтобы установить требования к классу, в который внедрится трейт. Поддерживаются общедоступные, защищённые и закрытые методы. До PHP 8.0.0 поддерживались только общедоступные и защищённые абстрактные методы.

Предостережение

Начиная с PHP 8.0.0 выдаётся фатальная ошибка, если сигнатура конкретного метода не следует правилам совместимости сигнатур. Раньше при несовпадении сигнатуры метода ошибка не выдавалась.

Пример #8 Пример установки требований к классу через абстрактный метод трейта

<?php

trait Hello
{
    public function sayHelloWorld()
    {
        echo 'Hello' . $this->getWorld();
    }

    abstract public function getWorld();
}

class MyHelloWorld
{
    private $world;

    use Hello;

    public function getWorld()
    {
        return $this->world;
    }

    public function setWorld($val)
    {
        $this->world = $val;
    }
}

?>

Статические члены трейта

В трейтах разрешается определять статические переменные, статические методы и статические свойства.

Замечание:

Начиная с PHP 8.1.0 прямой вызов статического метода или прямой доступ к статическому свойству в трейте устарели. Доступ к статическим методам и свойствам получают только в классе, в который внедрили трейт.

Пример #9 Статические переменные

<?php

trait Counter
{
    public function inc()
    {
        static $c = 0;
        $c = $c + 1;
        echo "$c\n";
    }
}

class C1
{
    use Counter;
}

class C2
{
    use Counter;
}

$o = new C1();
$o->inc();

$p = new C2();
$p->inc();

?>

Результат выполнения приведённого примера:

1
1

Пример #10 Статические методы

<?php

trait StaticExample
{
    public static function doSomething()
    {
        return 'Делаем что-нибудь';
    }
}

class Example
{
    use StaticExample;
}

echo Example::doSomething();

?>

Результат выполнения приведённого примера:

Doing something

Пример #11 Статические свойства

Предостережение

До PHP 8.3.0 дочерние классы наследовали статические свойства, которые родительские классы получали из трейта, даже если трейт явно внедрялся в дочерний класс. Начиная с PHP 8.3.0 статическое свойство трейта переопределяет в дочернем классе статическое свойство, которое дочерний класс унаследовал из родительского.

<?php

trait T
{
    public static $counter = 1;
}

class A
{
    use T;

    public static function incrementCounter()
    {
        static::$counter++;
    }
}

class B extends A
{
    use T;
}

A::incrementCounter();

echo A::$counter, "\n";
echo B::$counter, "\n";

?>

Результат выполнения приведённого примера в PHP 8.3:

2
1

Свойства

В трейтах доступно определение свойств.

Пример #12 Пример определения свойств в трейте

<?php

trait PropertiesTrait
{
    public $x = 1;
}

class PropertiesExample
{
    use PropertiesTrait;
}

$example = new PropertiesExample();
$example->x;

?>

В классе нельзя определять свойство с названием как у свойства трейта, если свойство класса несовместимо со свойством трейта по области видимости и типу, модификатору readonly и начальному значению, иначе PHP выдаёт фатальную ошибку.

Пример #13 Разрешение конфликтов

<?php

trait PropertiesTrait
{
    public $same = true;
    public $different1 = false;
    public bool $different2;
    public bool $different3;
}

class PropertiesExample
{
    use PropertiesTrait;

    public $same = true;
    public $different1 = true; // Фатальная ошибка
    public string $different2; // Фатальная ошибка
    readonly protected bool $different3; // Фатальная ошибка
}

?>

Константы

Начиная с версии PHP 8.2.0 в трейтах разрешили также определять константы.

Пример #14 Определение констант

<?php

trait ConstantsTrait
{
    public const FLAG_MUTABLE = 1;
    final public const FLAG_IMMUTABLE = 5;
}

class ConstantsExample
{
    use ConstantsTrait;
}

$example = new ConstantsExample;
echo $example::FLAG_MUTABLE;

?>

Результат выполнения приведённого примера:

1

В классе нельзя определять константу с названием как у константы трейта, если константа класса несовместима с константой трейта по области видимости, начальному значению и модификатору final, иначе PHP выдаёт фатальную ошибку.

Пример #15 Разрешение конфликтов

<?php

trait ConstantsTrait
{
    public const FLAG_MUTABLE = 1;
    final public const FLAG_IMMUTABLE = 5;
}

class ConstantsExample
{
    use ConstantsTrait;

    public const FLAG_IMMUTABLE = 5; // Фатальная ошибка
}

?>

Окончательные методы

Начиная с PHP 8.3.0 методы, которые импортировали из трейтов, разрешили делать окончательными через оператор as и модификатор final. Определение метода трейта окончательным запрещает дочерним классам переопределять метод. Но самому классу, в который импортировали трейт и в котором сделали метод окончательным, по-прежнему доступно переопределение метода.

Пример #16 Определение метода окончательным путём добавления модификатора final при внедрении трейта

<?php

trait CommonTrait
{
    public function method()
    {
        echo 'Привет';
    }
}

class FinalExampleA
{
    use CommonTrait {
        CommonTrait::method as final; // Начиная с PHP 8.3.0 модификатор final
                                      // запретит переопределять метод в дочерних классах
    }
}

class FinalExampleB extends FinalExampleA
{
    public function method() {}
}

?>

Вывод приведённого примера будет похож на:

Fatal error: Cannot override final method FinalExampleA::method() in ...