Совсем недавно вышла первая beta php 5.4, а пока я писал топик подоспела и вторая. Одно из нововведений в 5.4 – это traits (типажи). Предлагаю разобраться во всех деталях в том, что же типажи из себя представляют в php.
Простой пример типажа, чтобы не заглядывать в Википедею:
Как видно, к классу
Но во всём есть свои детали.
В общем и целом всё просто. Типажей можно подключить к классу неограниченное кол-во через одну или несколько конструкций
Дополнительно в блоке (
Сам типаж записывается, как
Более сложный пример:
Тут важно обратить внимание на два момента. Во-первых, блок после
Чтобы не возникало путаницы, хорошей практикой будет записать сначала все типажи через запятую, а затем на отдельной строке правила перекрытия и alias. Либо описывать все правила для типажа рядом с его подключением. Выбор за вами.
Во-вторых, обратите внимание на список методов, в списке остался
Типажи инициализируются, как и классы, динамически. При большом желании можно писать так:
До этого, я оперировал методами, но типаж может включать в себя и свойства, которые будут добавлены в класс. В этом плане «типажи» в php – это скорее mixin.
Сразу предлагаю хорошую практику, чтобы однажды не оказалось, что свойство
Важно понимать, как будут разрешаться различные вызовы внутри типажа. В этом поможет правило думать о подключении типажа, как о «copy-paste» кода в целевой класс. В самом первом примере, интерпретатор как бы сделал «copy-paste» метода
Внутри методов типажа доступны все свойства объекта для обращения напрямую, никаких дополнительных областей видимости не добавляется. Можно было бы получить просто
В типаже можно объявлять статические методы, но нельзя объявлять статические свойства. Внутри статических методов можно использовать, как статическое связывание (self::), так и динамическое (static::), всё будет работать так, как будто вызвано из метода класса («copy-paste»).
Ограничение на хранение статических свойств обойти можно, как именно покажу позже с обращением к магии.
Метод описанный в классе перекрывает метод из типажа. Но если какой-то метод описан в родительском классе, а в дочернем классе подключён типаж с таким же методом, он перекроет метод из родительского (снова вспоминаем «copy-paste»).
Если в нескольких, указанных у класса типажах, используются одинаковые методы, php выдаст ошибку на этапе инициализации класса:
Хитрая ошибка может быть в случае, когда в классе тоже определён метод, вызвавший коллизию, в таком случае php пропустит эту проверку, т.к. он проверяет только «выжившие» методы типажа:
В этом моменте нас поджидают неприятные проблемы. Сразу пример:
Поясняю. В общем случае при пересечении свойств типажей между собой или свойств типажа и класса выдаётся ошибка. Но зачем-то для «совместимых» свойств делается исключение и они работают по принципу «кто последний, тот и прав». Поэтому в классе
Совместимыми считаются значения нестрогое сравнение которых даёт true, а так как в php при этом много неявных преобразований, могут быть неприятные ошибки при использовании строго сравнения возвращаемых значений.
Так что практика с префиксами, предложенная выше, будет полезна и в таких случаях. Я же надеюсь что эту часть реализации ещё пересмотрят к релизу.
Если следовать мнемоническому правилу trait == «copy-paste», с ошибками становится сразу всё понятно:
Объект уже не знает, откуда у него взялся метод в котором был Notice или Exception, но это можно узнать в stack trace по строкам кода, в которых были вызовы. Если хранить типажи в отдельных файлах определить будет ещё проще.
Немного
Покажу пару грязных приёмов с типажами, используйте их на свой страх и риск.
Чтобы удалить метод типажа, например, когда ему был задан alias, можно сделать так:
Но в таком подходе таится большая опасность, т.к. одни методы типажа потенциально могут вызывать другие методы:
При переименовании типаж ничего не знает о том, что метод был переименован. Поэтому по-умолчанию при указании alias'а сохраняется оригинальный метод.
С помощью похожего трюка можно реализовать «наследование» в типажах c возможностью вызова «родительских» методов.
Чтобы сгладить это магическое безобразие покажу один полезный пример. Часто в виде типажа приводят Singleton, хотя без возможности задания в типаже статической переменной сделать его будет не так просто, как кажется на первый взгляд. Можно воспользоваться двумя хитростями.
Первая – получить внутри вызываемого метода название класса, к которому он был вызван, а затем в качестве хранилища воспользоваться отдельным классом со статическим методом, примерно так:
Вторая – воспользоваться толи фичей, толи багой php, которая связана с использованием ключевого слова
Простой пример типажа, чтобы не заглядывать в Википедею:
//определение типажа
trait Pprint
{
public function whoAmI()
{
return get_class($this) . ': ' . (string) $this;
}
}
class Human
{
use Pprint; //подключаем типаж, ключевое слово use
protected $_name = 'unknown';
public function __construct($name)
{
$this->_name = $name;
}
public function __toString()
{
return (string) $this->_name;
}
}
$a = new Human('Nikita');
echo $a->whoAmI(), PHP_EOL; //=> Human: Nikita
Как видно, к классу
Human
было добавлено поведение из типажа Pprint
.Но во всём есть свои детали.
Синтаксис
В общем и целом всё просто. Типажей можно подключить к классу неограниченное кол-во через одну или несколько конструкций
use
внутри определения класса. use
может быть указан в любом месте класса.Дополнительно в блоке (
{...}
) после use
можно:- назначить alias'ы к методам типажа (
Trait::method as myMethod
–method
из Trait будет дополнительно доступен, какmyMethod
); - указать перекрытие метода одного типажа, методом другого, если у них совпали названия (
TraitA::method insteadof TraitB
– будет использован методTraitA
вместо одноимённого методаTraitB
); - повысить или понизить доступ к методу из типажа, за исключение перевода метода в статический (
Trait::publicMethod as protected
), можно сразу с переименованием (Trait::publicMethod as protected _myProtectedMethod
).
Сам типаж записывается, как
trait
и может включать другие типажи, через указание их в ключевом слове use
. Cинтаксис и возможности аналогичны use
в классе.Более сложный пример:
trait Pprint
{
public function whoAmI()
{
return get_class($this) . ': ' . (string) $this;
}
}
trait Namer
{
//использование одного типажа в другом
use Pprint;
public function getMyName()
{
return $this->whoAmI();
}
public function getMyLastName()
{
return 'Unknown =(';
}
public function getMyNickname()
{
return preg_replace('/[^a-z]+/i', '_', strtolower($this->getMyName()));
}
}
trait SuperNamer
{
public function getMyLastName()
{
return 'Ask me';
}
}
class Human
{
use SuperNamer;
use Namer
{
SuperNamer::getMyLastName insteadof Namer;
Namer::getMyNickname as protected _getMyLogin;
}
protected $_name = 'unknown';
public function __construct($name)
{
$this->_name = $name;
}
public function __toString()
{
return (string) $this->_name;
}
public function getLogin()
{
return $this->_getMyLogin();
}
}
$a = new Human('Nikita');
echo join(', ', get_class_methods($a)), PHP_EOL;
//__construct, __toString, getLogin, getMyLastName,
//getMyName, getMyNickname, whoAmI
echo $a->getMyName(), PHP_EOL; //Human: Nikita
echo $a->getMyLastName(), PHP_EOL; //Ask me
echo $a->getLogin(), PHP_EOL; //human_nikita
echo $a->getMyNickname(), PHP_EOL; //human_nikita
Тут важно обратить внимание на два момента. Во-первых, блок после
use
кажется связанным с типажом около которого он описан, но это не так. Правила в блоке глобальные и могут быть объявлены в любом месте. Чтобы не возникало путаницы, хорошей практикой будет записать сначала все типажи через запятую, а затем на отдельной строке правила перекрытия и alias. Либо описывать все правила для типажа рядом с его подключением. Выбор за вами.
//так
use SuperNamer, Namer, Singleton, SomeOther
{
SuperNamer::getMyLastName insteadof Namer;
SomeOther::getSomething as private;
}
//либо так
use Namer;
use Singleton;
use SuperNamer
{
SuperNamer::getMyLastName insteadof Namer;
}
use SomeOther
{
SomeOther::getSomething as private;
}
Во-вторых, обратите внимание на список методов, в списке остался
getMyNickname
, а _getMyLogin
просто его alias с пониженным доступом. Можно исключить исходный метод совсем, но об этом ниже в разделе магии.Типажи инициализируются, как и классы, динамически. При большом желании можно писать так:
if ($isWin) {
trait A { /* … */}
} else {
trait A { /* … */}
}
Свойства в типажах
До этого, я оперировал методами, но типаж может включать в себя и свойства, которые будут добавлены в класс. В этом плане «типажи» в php – это скорее mixin.
trait WithId
{
protected $_id = null;
public function getId()
{
return $this->_id;
}
public function setId($id)
{
$this->_id = $id;
}
}
Сразу предлагаю хорошую практику, чтобы однажды не оказалось, что свойство
_id
в типаже конфликтует с используемым в классе или его потомках, свойства типажей записывать с префиксами:trait WithId
{
protected $_WithId_id = null;
protected $_WithId_checked = false;
//...
public function getId()
{
return $this->_WithId_id;
}
public function setId($id)
{
$this->_WithId_id = $id;
}
}
Область видимости
Важно понимать, как будут разрешаться различные вызовы внутри типажа. В этом поможет правило думать о подключении типажа, как о «copy-paste» кода в целевой класс. В самом первом примере, интерпретатор как бы сделал «copy-paste» метода
whoAmI
в класс Human
, соответственно все вызовы к parent
, self
, $this
будут работать также, как и вызов в методах класса. Исключение будут составлять некоторые магические константы, например внутри whoAmI
__METHOD__ === 'Pprint::whoAmI'.Внутри методов типажа доступны все свойства объекта для обращения напрямую, никаких дополнительных областей видимости не добавляется. Можно было бы получить просто
$this->_name
, вместо вызова __toString
. Однако стоит несколько раз подумать, прежде чем делать это, так как на сложных реализациях это внесёт не мало путаницы. Я бы рекомендовал всегда использовать понятные методы, при необходимости даже описать их в интерфейсе и «заставлять» классы его имплементировать.Статические методы и свойства
В типаже можно объявлять статические методы, но нельзя объявлять статические свойства. Внутри статических методов можно использовать, как статическое связывание (self::), так и динамическое (static::), всё будет работать так, как будто вызвано из метода класса («copy-paste»).
Ограничение на хранение статических свойств обойти можно, как именно покажу позже с обращением к магии.
Совпадение методов типажей между собой и с методами класса
Метод описанный в классе перекрывает метод из типажа. Но если какой-то метод описан в родительском классе, а в дочернем классе подключён типаж с таким же методом, он перекроет метод из родительского (снова вспоминаем «copy-paste»).
Если в нескольких, указанных у класса типажах, используются одинаковые методы, php выдаст ошибку на этапе инициализации класса:
trait A
{
public function abc() {}
}
trait B
{
public function abc() {}
}
class C
{
use A, B;
}
//Fatal error: Trait method abc has not been applied,
//because there are collisions with other trait methods
//on C in %FILE% on line %line%
На помощь приходит insteadof
, с помощью которого нужно будет разрешить все коллизии.Хитрая ошибка может быть в случае, когда в классе тоже определён метод, вызвавший коллизию, в таком случае php пропустит эту проверку, т.к. он проверяет только «выжившие» методы типажа:
trait A
{
public function abc() {}
}
trait B
{
public function abc() {}
}
class C
{
use A, B;
public function abc() {}
}
//OK
Когда-нибудь потом, перенеся метод abc
в родительский класс, получим странную ошибку по коллизии методов типажей, которая может сбить с толку. Так что, коллизии лучше разрешить заранее. (С другой стороны, если в коде методы типажа и класса совпадают, возможно что-то уже не так.)Совпадение свойств типажа со свойствами другого типажа и свойствами класса
В этом моменте нас поджидают неприятные проблемы. Сразу пример:
trait WithId
{
protected $_id = false;
//protected $_var = 'a';
public function getId()
{
return $this->_id;
}
//...
}
trait WithId2
{
protected $_id = null;
//protected $_var = null;
//...
}
class A
{
use WithId, WithId2;
}
class B
{
use WithId2, WithId;
}
class C
{
use WithId;
protected $_id = '0';
}
//
$a = new A();
var_dump($a->getId()); //NULL
$b = new B();
var_dump($b->getId()); //false
$c = new C();
var_dump($c->getId()); //false (!)
//Если раскомментировать $_var
// WithId and WithId2 define the same property ($_var)
// in the composition of A. However, the definition differs
// and is considered incompatible. Class was composed
// in %FILE% on line %LINE%
Поясняю. В общем случае при пересечении свойств типажей между собой или свойств типажа и класса выдаётся ошибка. Но зачем-то для «совместимых» свойств делается исключение и они работают по принципу «кто последний, тот и прав». Поэтому в классе
A
в getId
получилось NULL, а в классе B
– false. При этом свойства класса считаются ниже, чем свойство типажа (с методами равно наоборот) и в C
вместо ожидаемого '0' получим false.Совместимыми считаются значения нестрогое сравнение которых даёт true, а так как в php при этом много неявных преобразований, могут быть неприятные ошибки при использовании строго сравнения возвращаемых значений.
var_dump(null == false); //true
var_dump('0' == false); //true
var_dump('a' == null); //false
Так что практика с префиксами, предложенная выше, будет полезна и в таких случаях. Я же надеюсь что эту часть реализации ещё пересмотрят к релизу.
Ошибки и исключения в типажах
Если следовать мнемоническому правилу trait == «copy-paste», с ошибками становится сразу всё понятно:
<?php trait Slug
{
public function error()
{
echo $this->a; //5
}
public function someMethod()
{
$this->error();
}
public function testExc()
{
throw new Exception('Test'); //16
}
}
class Brain
{
use Slug;
public function plurk()
{
$this->testExc(); //25
}
}
error_reporting(E_ALL);
$b = new Brain();
$b->someMethod();
//Notice: Undefined property: Brain::$a in %FILE% on line 5
try {
$b->plurk(); //35
} catch(Exception $e) {
echo $e;
}
// exception 'Exception' with message 'Test' in %FILE%:16
// Stack trace:
// #0 %FILE%(25): Brain->testExc()
// #1 %FILE%(35): Brain->plurk()
// #2 {main}
Объект уже не знает, откуда у него взялся метод в котором был Notice или Exception, но это можно узнать в stack trace по строкам кода, в которых были вызовы. Если хранить типажи в отдельных файлах определить будет ещё проще.
Немного белой чёрной магии
Покажу пару грязных приёмов с типажами, используйте их на свой страх и риск.
Удаление метода типажа
Чтобы удалить метод типажа, например, когда ему был задан alias, можно сделать так:
trait A
{
public function a() {}
public function b() {}
}
trait B
{
public function d()
{
$this->e();
}
public function e() {}
}
class C
{
use A
{
//удаляем и переименовываем
A::b insteadof A;
A::b as c;
}
use B
{
//удаляем метод совсем
B::e insteadof B;
}
}
echo join(", ", get_class_methods('C')), PHP_EOL;
//a, c, d
Но в таком подходе таится большая опасность, т.к. одни методы типажа потенциально могут вызывать другие методы:
$c = new C();
$c->d();
//Fatal error: Call to undefined method C::e()
При переименовании типаж ничего не знает о том, что метод был переименован. Поэтому по-умолчанию при указании alias'а сохраняется оригинальный метод.
«Наследование» в типажах
С помощью похожего трюка можно реализовать «наследование» в типажах c возможностью вызова «родительских» методов.
trait Namer
{
public function getName()
{
return 'Name';
}
}
trait Namer2
{
public function getName()
{
return 'Name2';
}
}
trait Supernamer
{
use Namer, Namer2
{
Namer::getName insteadof Namer;
Namer::getName as protected _Namer_getName_;
Namer2::getName insteadof Namer2;
Namer2::getName as protected _Namer2_getName_;
}
public function getName()
{
return $this->_Namer_getName_() . $this->_Namer2_getName_();
}
}
Два способа реализовать Singleton с помощью типажей
Чтобы сгладить это магическое безобразие покажу один полезный пример. Часто в виде типажа приводят Singleton, хотя без возможности задания в типаже статической переменной сделать его будет не так просто, как кажется на первый взгляд. Можно воспользоваться двумя хитростями.
Первая – получить внутри вызываемого метода название класса, к которому он был вызван, а затем в качестве хранилища воспользоваться отдельным классом со статическим методом, примерно так:
trait Singleton {
static public function getInstance()
{
$class = get_called_class(); //работает аналогично static::
if (!Storage::hasInstance($class)) {
$new = new static();
Storage::setInstance($class, $new);
}
return Storage::getInstance($class);
}
}
Вторая – воспользоваться толи фичей, толи багой php, которая связана с использованием ключевого слова
static
при объявлении переменной. Эти переменные должны сохранять своё значение при вызовах метода, но видимо структура для хранения этих переменных инициализируется в каждом месте использования метода. В итоге получается такая схема:trait Singleton
{
static public function getInstance()
{
static $instance = null;
if ($instance === null) {
$instance = new static();
}
return $instance;
}
}
class MyClass
{
use Singleton;
}
class MyExtClass extends MyClass {}
echo get_class(MyClass::getInstance()), PHP_EOL; //MyClass
echo get_class(MyExtClass::getInstance()), PHP_EOL; //MyExtClass