W3docs

Объектно-ориентированное программирование в 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, и помните, что код трейта встраивается в каждый класс, который его использует. При разумном использовании трейты позволяют держать сквозное поведение — логирование, сериализацию, счётчики — в одном месте, а не разбросанным по копиям.

Практика

Практика
Каковы характеристики трейтов PHP?
Каковы характеристики трейтов PHP?
Was this page helpful?