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