Трейты

Начиная с версии 5.4.0, PHP реализует метод для повторного использования кода под названием трейт (trait).

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

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

Пример #1 Пример использования трейта

<?php
trait ezcReflectionReturnInfo {
    function 
getReturnType() { /*1*/ }
    function 
getReturnDescription() { /*2*/ }
}

class 
ezcReflectionMethod extends ReflectionMethod {
    use 
ezcReflectionReturnInfo;
    
/* ... */
}

class 
ezcReflectionFunction extends ReflectionFunction {
    use 
ezcReflectionReturnInfo;
    
/* ... */
}
?>

Приоритет

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

Пример #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 
HelloWorld;
    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.

Класс Aliased_Talker применяет оператор as чтобы получить возможность использовать имплементацию bigTalk из B под дополнительным псевдонимом talk.

<?php
trait {
    public function 
smallTalk() {
        echo 
'a';
    }
    public function 
bigTalk() {
        echo 
'A';
    }
}

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

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

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

Замечание:

До PHP 7.0 определение свойства в классе с таким же именем, что и в трейте, приводило к ошибке уровня E_STRICT, даже если определение полностью совпадало (та же область видимости и такое же начальное значение).

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

Используя синтаксис оператора as можно также настроить видимость метода в использующем трейт классе.

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

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

// Изменение видимости метода 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 
HelloWorld;
}

class 
MyHelloWorld {
    use 
HelloWorld;
}

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

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

Hello World!

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

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

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

Конкретный класс исполняет эти требования путем определения конкретного метода с тем же именем; при этом сигнатура метода может отличаться.

Пример #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;
    }
}
?>

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

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

Пример #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(); // echo 1
$p = new C2(); $p->inc(); // echo 1
?>

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

<?php
trait StaticExample {
    public static function 
doSomething() {
        return 
'Что-либо делаем';
    }
}

class 
Example {
    use 
StaticExample;
}

Example::doSomething();
?>

Свойства

Трейты могут также определять свойства.

Пример #11 Определение свойств

<?php
trait PropertiesTrait {
    public 
$x 1;
}

class 
PropertiesExample {
    use 
PropertiesTrait;
}

$example = new PropertiesExample;
$example->x;
?>

Если трейт определяет свойство, то класс не может определить свойство с таким же именем, кроме случаев полного совпадения (те же начальное значение и модификатор видимости), иначе будет сгенерирована фатальная ошибка. До PHP 7.0.0 при определении свойства в классе, полностью идентичное свойству трейта, выдавалась ошибка уровня E_STRICT.

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

<?php
trait PropertiesTrait {
    public 
$same true;
    public 
$different false;
}

class 
PropertiesExample {
    use 
PropertiesTrait;
    public 
$same true// Допустимо с PHP 7.0.0. В более ранних версиях ошибка уровня E_STRICT
    
public $different true// Фатальная ошибка
}
?>