Трейты в PHP
PHP поддерживает только одиночное наследование: дочерний класс не может наследовать от нескольких классов сразу, только от одного единственного родителя. Однако в большинстве случаев было бы полезно наследовать от нескольких классов. Например, было бы желательно наследовать методы от нескольких разных классов, чтобы предотвратить дублирование кода. Трейты используются для восполнения этого пробела, позволяя нам повторно использовать одни и те же свойства и методы в нескольких классах.
Что такое трейты?
Трейты (англ. trait) используются для объявления методов, которые можно использовать в нескольких классах. Трейты могут иметь методы и абстрактные методы, которые могут использоваться в нескольких классах. Методы трейтов могут иметь любой модификатор доступа (публичный, приватный или защищенный).
Синтаксис трейта такой же как и у класса, за исключением того, что имя трейта нужно объявлять с помощью ключевого слова trait :
Синтаксис
Экземпляр трейта, как и абстрактного класса, нельзя создать — трейты предназначены только для подключения к другим классам.
Синтаксис
Давайте посмотрим на пример в котором объявим один трейт и подключим к классу:
Пример
Результат выполнения кода:
По сути, трейт — это просто способ скопировать и вставить код во время выполнения.
Использование нескольких трейтов
Пример
Результат выполнения кода:
Чем трейты отличаются от интерфейсов?
Трейты очень похожи на интерфейсы. И трейты, и интерфейсы обычно просты, лаконичны и мало используются без реально реализованного класса. Однако разница между ними есть.
Интерфейс — это контракт, в котором говорится, что «этот объект может делать это», тогда как трейт дает объекту возможность делать это.
Другими словами, если код ООП касается планирования и проектирования, то интерфейс — это план, а объект — полностью построенный дом. Между тем, трейты — это просто способ помочь построить дом, спроектированный по плану (интерфейсу).
Интерфейсы — это спецификации, которые можно проверить используя оператор instanceof (является ли текущий объект экземпляром указанного класса).
Вы должны использовать трейты только тогда, когда несколько классов имеют одну и ту же функциональность (вероятно, продиктованную одним и тем же интерфейсом). Нет смысла использовать трейт для обеспечения функциональности для одного класса: это только запутывает то, что делает класс.
Пример
Результат выполнения кода:
Основное отличие состоит в том, что с интерфейсами вы должны определить фактическую реализацию каждого метода в каждом классе, реализующем указанный интерфейс, поэтому вы можете иметь множество классов, реализующих один и тот же интерфейс, но с различным поведением. В то время как трейты — это просто фрагменты кода, введенные в класс. Еще одно важное отличие состоит в том, что методы трейтов могут быть только методами класса или статическими методами, в отличие от методов интерфейса, которые также могут (и обычно являются) методами экземпляра.
Переопределение унаследованных методов
Как описано в Руководстве: Унаследованный метод от базового класса переопределяется методом, вставленным с помощью трейта. Порядок приоритета таков — методы текущего класса переопределяют методы трейта, которые в свою очередь переопределяют унаследованные методы.
Итак, рассмотрим следующий сценарий:
Пример
Результат выполнения кода:
При создании экземпляра MyClass, описанного выше, происходит следующее:
Заключение
Трейты в PHP
Трейты используются когда нужно реализовать ряд общих методов для нескольких классов, не присваивая экземплярам класса новый тип, как при использовании абстрактных классов или интерфейсов.
Трейты решают проблему дублирования кода.
Давайте рассмотрим пример использования трейта.
Важно понимать то, что трейты не присваивают экземплярам класса (объектам) новый тип.
Невозможно создать самостоятельный экземпляр трейта.
Использование нескольких трейтов
С этим моментом всё просто.
Совместное использование трейтов и интерфейсов
Конфликты имён в трейтах, insteadof.
Давайте на примере рассмотрим как это работает:
Псевдонимы для переопределённых методов
Вот как это работает:
Статические методы в трейте
Нам ничего не мешает объявлять методы в трейте статическими. Давайте рассмотрим пример трейта со статическими методами.
Я и в прошлых примерах обращался к методу трейта напрямую, без создания объекта. В этом примере я только лишь объявил метод трейта как статический.
Доступ к свойствам базового класса
Абстрактные методы в классах
Если в трейте объявляется абстрактный метод, то этот метод должен быть реализован в базовом классе.
В следующем примере мы гарантируем наличие свойства в базовом классе объявив в трейте абстрактный метод.
Изменения прав доступа к методам трейта
Очевидно, что внутри трейта мы можем использовать любой любой модификатор доступа ( public, private, protected ) для метода. Но, кроме этого, у нас есть возможность в классе менять этот модификатор на другой. Для этого в оператор use после слова as можно указать новый модификатор.
Итоги
Кратко о том, что мы выучили.
Примеси в PHP (trait) // PHP
С версии 5.4 в PHP появился такой интересный механизм как примеси (trait), который по задумке разработчиков должен помочь разруливать ситуации когда уж очень хочется применить множественное наследование, но нельзя. Вот о некоторых подобных ситуациях я и расскажу далее.
Примеры не надуманные, а вполне рабочие из фреймворка Bluz 😉
Теория
Тут будет краткий пересказ официальной документации в моей интерпретации, если не интересно — промотайте чуть дальше…
Хотя нет, лучше останьтесь и прочитайте, ведь мне абсолютно не нравится подача этого материала в официальной документации, для понимания примесей надо разговор начинать с необходимости данного нововведения в PHP. Начну издалека, вот один из классических примеров ООП: есть следующие классы – абстрактный класс Мебель, который лишь знает что у мебели есть размеры, Стол с площадью столешницы и Стул с некой максимально-возможной нагрузкой, в довесок есть Диван, пока просто диван:
Вот такая у нас простая мебель получается, теперь давайте размышлять как мы можем расширить данные классы, и каким образом мы бы это делали:
Давайте-ка распишем данный подход в коде:
Да тут невооруженным взглядом видно копипасту, и очень хотелось бы избавится от неё, хотелось бы реализацию требований 3 и 4 закинуть в отдельный класс, и наследовать его, но в PHP нет множественного наследования, может быть только один класс предок. И вот в PHP 5.4 на сцену выходят примеси (trait), чем-то они схожи на классы, но лишь издалека, примеси лишь группируют некий набор функционала под одной вывеской, но не более. Давайте таки опишем необходимый функционал в примесях:
Теперь данный примеси легко можно подключить в наших классах:
Как видим, ничего сложного и результат читаемый, и код аккуратен. Как по мне – примеси – это копипаст на уровне языка программирования, и главное, не следует их использовать как альтернативу классическому наследованию, это лишь дополнительный инструмент, помогающий обойти ограничение единого наследования.
Реализация шаблона Singleton
Можно много спорить о данном шаблоне, есть у него и плюсы и минусы, но речь не об этом, а о его реализации, при чём так, в один use 😉
Чтобы это стало возможным следует реализовать вот такую примесь:
Полный листинг класса можно найти в репозитории – Bluz/Common/Singleton.php. Пример не претендует на универсальность, но он юзабелен и имеет право на жизнь.
Обратите внимание, реализация шаблона Singleton никоим образом не обязует вас на хоть какие-то объединения классов под одним предком, т.к. это выглядило бы очень странно, а вот для избавления от практики копирования функционала из класса в класс нам и нужен данный trait
Реализация интерфейса инициализации
Теперь попробуем заюзать данную примесь в простом шаблонизаторе:
А вот и пример использования:
Примеси нам дают возможность не только описать интерфейс, но и фактически реализовать его, без необходимости копировать идентичный код из класса в класс
Реализация помощников класса
О чём это я, да о достаточно популярном приёме, когда функционал одного класса разделяют по различным классам и функциям с ленивой инициализацией, самый наглядный пример — это помощники View в Zend Framework. В фреймворке Bluz данный подход реализован в одном trait:
Для примера будем использовать всё тот же шаблонизатор:
В качестве ленивого помощника у нас будет анонимная функция:
Теперь можно пользоваться:
Код сокращён и упрощён для наглядности
Выводы
Как можно заметить, trait’ы можно и нужно использовать, ведь таким образом мы сокращаем объём кода, который нам потребуется поддерживать, да и метод копи-пасты уже давно должен был кануть в лету, а с появлением примесей вам уж не будет оправдания 🙂
Если у вас есть примеры использования примесей, прошу — оставляйте ссылки.
Трейты в PHP
Нужные для использования в классе трейты можно указать через запятую:
Приоритет методов при работе с трейтами.
— члены из текущего класса переопределяют одноименные методы в трейте,
— члены из трейта переопределяют унаследованные классом методы. То есть трейт имеет преимущество перед классом который наследуем.
Конфликты трейтов.
Ошибки могут быть когда подключается несколько трейтов, содержащие одни и те же методы. Или когда класс наследует у другого класса с подключением трейта, который уже был подключен в родительском классе.
Для разрешения конфликтов необходимо использовать оператор insteadof при подключении трейтов для того, чтобы точно выбрать один из конфликтных методов.
Внутри тела «use» мы использовали ключевое слово insteadof, слева от которого указывается трейт, метод которого будем использовать и имя самого метода, которые разделяются двойным двоеточием. В правой части указывается имя трейта, метод которого должен быть заменён.
Если же второй одноименный метод (из другого трейта) нам тоже нужен, то можно применить псевдоним имени используя ключевое слово as :
Статические методы и свойства
Используются так же как и в классах:
Доступ к свойствам базового класса.
В трейтах для доступа к свойствам базового класса можно использовать псевдопеременную $this.
Изменения прав доступа к методам трейта.
Внутри трейта мы можем использовать любой модификатор доступа (public, private, protected) для методов. Но, кроме этого, есть возможность в классе менять этот модификатор на другой. Для этого в теле use после слова as можно указать новый модификатор.
Traits в php 5.4. Разбираем детали реализации
Совсем недавно вышла первая beta php 5.4, а пока я писал топик подоспела и вторая. Одно из нововведений в 5.4 – это traits (типажи). Предлагаю разобраться во всех деталях в том, что же типажи из себя представляют в php.
Но во всём есть свои детали.
Синтаксис
В общем и целом всё просто. Типажей можно подключить к классу неограниченное кол-во через одну или несколько конструкций use внутри определения класса. use может быть указан в любом месте класса.
Более сложный пример:
Тут важно обратить внимание на два момента. Во-первых, блок после use кажется связанным с типажом около которого он описан, но это не так. Правила в блоке глобальные и могут быть объявлены в любом месте.
Типажи инициализируются, как и классы, динамически. При большом желании можно писать так:
Свойства в типажах
Область видимости
Статические методы и свойства
В типаже можно объявлять статические методы, но нельзя объявлять статические свойства. Внутри статических методов можно использовать, как статическое связывание (self::), так и динамическое (static::), всё будет работать так, как будто вызвано из метода класса («copy-paste»).
Ограничение на хранение статических свойств обойти можно, как именно покажу позже с обращением к магии.
Совпадение методов типажей между собой и с методами класса
Метод описанный в классе перекрывает метод из типажа. Но если какой-то метод описан в родительском классе, а в дочернем классе подключён типаж с таким же методом, он перекроет метод из родительского (снова вспоминаем «copy-paste»).
Хитрая ошибка может быть в случае, когда в классе тоже определён метод, вызвавший коллизию, в таком случае php пропустит эту проверку, т.к. он проверяет только «выжившие» методы типажа:Когда-нибудь потом, перенеся метод abc в родительский класс, получим странную ошибку по коллизии методов типажей, которая может сбить с толку. Так что, коллизии лучше разрешить заранее. (С другой стороны, если в коде методы типажа и класса совпадают, возможно что-то уже не так.)
Совпадение свойств типажа со свойствами другого типажа и свойствами класса
В этом моменте нас поджидают неприятные проблемы. Сразу пример:
Поясняю. В общем случае при пересечении свойств типажей между собой или свойств типажа и класса выдаётся ошибка. Но зачем-то для «совместимых» свойств делается исключение и они работают по принципу «кто последний, тот и прав». Поэтому в классе A в getId получилось NULL, а в классе B – false. При этом свойства класса считаются ниже, чем свойство типажа (с методами равно наоборот) и в C вместо ожидаемого ‘0’ получим false.
Совместимыми считаются значения нестрогое сравнение которых даёт true, а так как в php при этом много неявных преобразований, могут быть неприятные ошибки при использовании строго сравнения возвращаемых значений.
Так что практика с префиксами, предложенная выше, будет полезна и в таких случаях. Я же надеюсь что эту часть реализации ещё пересмотрят к релизу.
Ошибки и исключения в типажах
Если следовать мнемоническому правилу trait == «copy-paste», с ошибками становится сразу всё понятно:
Объект уже не знает, откуда у него взялся метод в котором был Notice или Exception, но это можно узнать в stack trace по строкам кода, в которых были вызовы. Если хранить типажи в отдельных файлах определить будет ещё проще.
Немного белой чёрной магии
Покажу пару грязных приёмов с типажами, используйте их на свой страх и риск.
Удаление метода типажа
Чтобы удалить метод типажа, например, когда ему был задан alias, можно сделать так:
Но в таком подходе таится большая опасность, т.к. одни методы типажа потенциально могут вызывать другие методы:
При переименовании типаж ничего не знает о том, что метод был переименован. Поэтому по-умолчанию при указании alias’а сохраняется оригинальный метод.
«Наследование» в типажах
С помощью похожего трюка можно реализовать «наследование» в типажах c возможностью вызова «родительских» методов.
Два способа реализовать Singleton с помощью типажей
Чтобы сгладить это магическое безобразие покажу один полезный пример. Часто в виде типажа приводят Singleton, хотя без возможности задания в типаже статической переменной сделать его будет не так просто, как кажется на первый взгляд. Можно воспользоваться двумя хитростями.
Первая – получить внутри вызываемого метода название класса, к которому он был вызван, а затем в качестве хранилища воспользоваться отдельным классом со статическим методом, примерно так:
Вторая – воспользоваться толи фичей, толи багой php, которая связана с использованием ключевого слова static при объявлении переменной. Эти переменные должны сохранять своё значение при вызовах метода, но видимо структура для хранения этих переменных инициализируется в каждом месте использования метода. В итоге получается такая схема:

Трейты в PHP


