Объектно-ориентированное программирование в PHP: понимание трейтов
Изучите трейты PHP: объявление и использование, комбинирование трейтов, разрешение конфликтов методов с insteadof и as, абстрактные и статические члены.
Трейт — это многоразовый блок методов (и свойств), который можно подмешать в любой класс. Трейты решают конкретную проблему PHP: класс может наследовать только одного родителя, поэтому когда два несвязанных класса должны совместно использовать одно и то же поведение, одиночного наследования недостаточно. Трейты позволяют горизонтально разделять поведение — между классами, не имеющими отношений «родитель–потомок».
В этой главе рассматриваются: что такое трейты, как их объявлять и использовать, как комбинировать несколько трейтов, как PHP разрешает конфликты имён методов, и подводные камни (абстрактные методы, статические члены, свойства), на которых легко споткнуться.
Что такое трейт?
Трейт выглядит почти как класс, но объявляется с ключевым словом trait вместо class. Ключевые отличия:
- Трейт нельзя создать самостоятельно — нет
new MyTrait(). - Трейт используется внутри класса с помощью ключевого слова
use. Его методы затем копируются в класс, как если бы вы написали их там сами. - Класс может использовать любое количество трейтов, а трейт может использовать другие трейты.
Думайте о трейте как о копировании и вставке с помощью компилятора: код трейта буквально встраивается в каждый класс, который его использует.
Объявление и использование трейта
Объявите трейт с помощью trait, затем подключите его в класс с помощью use:
<?php
trait Greetable
{
public function greet(): string
{
return "Hello, my name is {$this->name}.";
}
}
class User
{
public function __construct(public string $name) {}
use Greetable;
}
$user = new User("Ada");
echo $user->greet();
// Output: Hello, my name is Ada.Обратите внимание, что трейт ссылается на $this->name, не определяя его. Трейт работает в контексте класса, который его использует, поэтому он может опираться на свойства и методы, предоставляемые этим классом.
Использование нескольких трейтов
Класс может использовать несколько трейтов одновременно. Разделите их запятыми или укажите каждый с отдельным оператором use:
<?php
trait Loggable
{
public function log(string $message): string
{
return "[LOG] " . $message;
}
}
trait Jsonable
{
public function toJson(): string
{
return json_encode(get_object_vars($this));
}
}
class Order
{
use Loggable, Jsonable;
public function __construct(public int $id, public float $total) {}
}
$order = new Order(7, 49.99);
echo $order->log("created") . PHP_EOL;
echo $order->toJson();
// Output:
// [LOG] created
// {"id":7,"total":49.99}Разрешение конфликтов с помощью insteadof и as
Если два трейта определяют метод с одинаковым именем, PHP вызывает фатальную ошибку, если не указать, какой из них использовать. Используйте insteadof, чтобы выбрать победителя, и as, чтобы сохранить второй под псевдонимом:
<?php
trait FileStorage
{
public function save(): string
{
return "Saved to a file.";
}
}
trait DatabaseStorage
{
public function save(): string
{
return "Saved to the database.";
}
}
class Report
{
use FileStorage, DatabaseStorage {
DatabaseStorage::save insteadof FileStorage;
FileStorage::save as saveToFile;
}
}
$report = new Report();
echo $report->save() . PHP_EOL; // DatabaseStorage wins
echo $report->saveToFile(); // still reachable via the alias
// Output:
// Saved to the database.
// Saved to a file.Ключевое слово as также может изменять видимость метода, например protected reset as private — удобно, когда нужно сделать метод трейта доступным внутри, но не как часть публичного API.
Абстрактные и статические члены
Трейты могут делать больше, чем просто хранить конкретные методы экземпляра.
- Абстрактные методы позволяют трейту требовать от использующего класса реализации чего-либо. Именно так трейт объявляет зависимость от своего хоста.
- Статические методы и свойства ведут себя как обычные статические члены, но каждый использующий класс получает свою собственную копию любого статического свойства.
<?php
trait Counter
{
private static int $count = 0;
public static function increment(): int
{
return ++self::$count;
}
// The using class MUST provide this:
abstract public function label(): string;
}
class PageView
{
use Counter;
public function label(): string
{
return "views";
}
}
echo PageView::increment() . PHP_EOL; // 1
echo PageView::increment() . PHP_EOL; // 2
echo (new PageView())->label(); // views
// Output:
// 1
// 2
// viewsТрейты, наследование и интерфейсы
Выбирайте правильный инструмент:
- Наследование (
extends) моделирует отношение «является» и предоставляет одного родителя. Используйте его, когда классы действительно разделяют иерархию типов. См. PHP Наследование. - Интерфейсы определяют контракт — какие методы существуют — но не содержат реализации. См. PHP Интерфейсы.
- Трейты предоставляют реализацию, которую можно подмешать в несвязанные классы. Распространённый паттерн — интерфейс + трейт: интерфейс объявляет возможность, трейт предоставляет код по умолчанию.
Если вы только знакомитесь с этими концепциями, начните с PHP Классы и объекты и PHP Абстрактные классы.
Распространённые ловушки
- Конфликты имён молчат до поры до времени. Объединение трейтов, определяющих один и тот же метод, вызывает фатальную ошибку; всегда разрешайте их с помощью
insteadof/as. - Трейты встраиваются, а не наследуются. Метод, определённый непосредственно в классе, переопределяет версию трейта, а версия трейта переопределяет всё, унаследованное от родительского класса.
- Статические свойства принадлежат каждому классу отдельно. Два класса, использующие один трейт, не разделяют его статическое состояние — каждый получает отдельную копию.
- Не злоупотребляйте ими. Трейт, которому требуется много свойств от хоста, часто сигнализирует о том, что лучше подойдёт композиция (реальный объект) или наследование.
Заключение
Трейты дают PHP чистый способ совместно использовать реализации методов в несвязанных классах, обходя ограничение одиночного наследования. Объявляйте их с помощью trait, подключайте с помощью use, разрешайте конфликты с помощью insteadof и as, и помните, что код трейта встраивается в каждый класс, который его использует. При разумном использовании трейты позволяют держать сквозное поведение — логирование, сериализацию, счётчики — в одном месте, а не разбросанным по копиям.