- Traits в php 5.4. Разбираем детали реализации
- Синтаксис
- Свойства в типажах
- Область видимости
- Статические методы и свойства
- Совпадение методов типажей между собой и с методами класса
- Совпадение свойств типажа со свойствами другого типажа и свойствами класса
- Ошибки и исключения в типажах
- Немного белой чёрной магии
- Удаление метода типажа
- «Наследование» в типажах
- Два способа реализовать Singleton с помощью типажей
- Traits php что это
Traits в php 5.4. Разбираем детали реализации
Совсем недавно вышла первая beta php 5.4, а пока я писал топик подоспела и вторая. Одно из нововведений в 5.4 – это traits (типажи). Предлагаю разобраться во всех деталях в том, что же типажи из себя представляют в 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 может быть указан в любом месте класса.
- назначить alias’ы к методам типажа ( Trait::method as myMethod – method из Trait будет дополнительно доступен, как myMethod );
- указать перекрытие метода одного типажа, методом другого, если у них совпали названия ( TraitA::method insteadof TraitB – будет использован метод TraitA вместо одноимённого метода TraitB );
- повысить или понизить доступ к методу из типажа, за исключение перевода метода в статический ( Trait::publicMethod as protected ), можно сразу с переименованием ( Trait::publicMethod as protected _myProtectedMethod ).
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
Во-вторых, обратите внимание на список методов, в списке остался getMyNickname , а _getMyLogin просто его alias с пониженным доступом. Можно исключить исходный метод совсем, но об этом ниже в разделе магии.
Типажи инициализируются, как и классы, динамически. При большом желании можно писать так:
Свойства в типажах
До этого, я оперировал методами, но типаж может включать в себя и свойства, которые будут добавлены в класс. В этом плане «типажи» в 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», с ошибками становится сразу всё понятно:
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
Объект уже не знает, откуда у него взялся метод в котором был Notice или Exception, но это можно узнать в stack trace по строкам кода, в которых были вызовы. Если хранить типажи в отдельных файлах определить будет ещё проще.
Немного белой чёрной магии
Покажу пару грязных приёмов с типажами, используйте их на свой страх и риск.
Удаление метода типажа
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
Traits php что это
Traits представляют группу методов, которые могут быть добавлены в классы. Traits позволяют определять блоки функционала и многократно повторно использовать в классах без необходимости усложнять код классов, которые используют эти методы.
Traits определяются с помощью ключевого словва trait , после которого идет название трейта:
Traits могут содержать только статические и нестатические методы:
trait Printer < public function printSimpleText($text) < echo "$text
"; > public function printHeaderText($text) < echo "$text"; > >
Для применения классов трейта применяется оператор use , после которого указывается добавляемый трейт:
trait Printer < public function printSimpleText($text) < echo "$text
"; > public function printHeaderText($text) < echo "$text"; > > class Message < use Printer; >$myMessage = new Message(); $myMessage->printSimpleText("Hello World!"); $myMessage->printHeaderText("Hello PHP 8");
После добавления трейта с помощью оператора use класс может использовать его методы, как-будто они определены в самом этом классе.
Следует учитывать, что при наследовании методы трейта переопределяют унаследованные методы с тем же именем:
class Data < function print() < echo "Print from Data"; >> trait Printer < function print() < echo "Print from Printer"; >> class Message extends Data < use Printer; >$myMessage = new Message(); $myMessage->print(); // Print from Printer