Трейты

Способ, которым в языке 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 ...