Совсем недавно вышла первая 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
