
Traits in php 5.4. Parse implementation details
- Tutorial
Most recently , the first beta php 5.4 was released , and while I was writing the topic , the second one arrived in time . One of the innovations in 5.4 is traits. I propose to understand all the details of what traits are like in php.
A simple example of a trait so as not to look into Wikipedia :
As you can see, the
But everything has its own details.
In general, everything is simple. You can connect unlimited number of types to a class through one or several constructions
Additionally, in block (
The type itself is recorded, as
More complex example:
It is important to pay attention to two points. Firstly, the block after
To avoid confusion, it is a good practice to write all traits first, separated by commas, and then overlap rules and alias on a separate line. Or describe all the rules for the type next to its connection. The choice is yours.
Secondly, pay attention to the list of methods, the list remains
Traits are initialized, like classes, dynamically. With a strong desire, you can write like this:
Before that, I operated on methods, but the type may include properties that will be added to the class. In this regard, “traits” in php are more like mixin.
Immediately I suggest good practice, so that one day it does not turn out that the property
Важно понимать, как будут разрешаться различные вызовы внутри типажа. В этом поможет правило думать о подключении типажа, как о «copy-paste» кода в целевой класс. В самом первом примере, интерпретатор как бы сделал «copy-paste» метода
Внутри методов типажа доступны все свойства объекта для обращения напрямую, никаких дополнительных областей видимости не добавляется. Можно было бы получить просто
You can declare static methods in a type, but you cannot declare static properties. Inside static methods, you can use both static binding (self: :) and dynamic binding (static: :), everything will work as if called from a class method (“copy-paste”).
The restriction on the storage of static properties can be circumvented, as I will show later with an appeal to magic.
The method described in the class overrides the method from the type. But if some method is described in the parent class, and a type with the same method is connected in the child class, it will override the method from the parent (again, remember “copy-paste”).
If several methods specified by the class use the same methods, php will throw an error at the initialization stage of the class:
A tricky mistake can be when the method that caused the collision is also defined in the class, in which case php will skip this check, because he checks only the "surviving" trait methods:
At this moment, unpleasant problems await us. Immediately an example:
I explain. In the general case, when intersecting the properties of the types between themselves or the properties of the type and the class, an error is thrown. But for some reason an exception is made for "compatible" properties and they work on the principle of "who is last, that is right." Therefore, in the class
Values that are considered compatible are non-strict comparisons of which give true, and since php has a lot of implicit conversions, there can be unpleasant errors when using strictly comparisons of returned values.
So the practice with prefixes proposed above will be useful in such cases. I hope that this part of the implementation will be revised for release.
If you follow the mnemonic rule trait == “copy-paste”, everything becomes immediately clear with errors:
The object no longer knows where it came from in which there was a Notice or Exception, but this can be found in the stack trace by the lines of code in which the calls were. If you store traits in separate files, it will be even easier to determine.
A bit of
I will show a couple of dirty tricks with types, use them at your own peril and risk.
To remove a trait method, for example, when it was given alias, you can do this:
But in this approach, there is a great danger, because some type methods can potentially call other methods:
When renaming, the type does not know that the method has been renamed. Therefore, by default, when specifying alias, the original method is saved.
Using a similar trick, you can implement “inheritance” in traits with the ability to call “parent” methods.
To smooth out this magical disgrace I will show one useful example. Often, Singleton is given in the form of a type, although without the possibility of setting a static variable in the type, making it will not be as simple as it seems at first glance. You can use two tricks.
The first is to get the name of the class to which it was called inside the called method, and then use a separate class with a static method as storage, something like this:
The second is to use feature roofing felts, php roofing felts, which is associated with the use of a keyword
A simple example of a trait so as not to look into Wikipedia :
//определение типажа
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
As you can see, the
Human
type behavior was added to the class Pprint
. But everything has its own details.
Syntax
In general, everything is simple. You can connect unlimited number of types to a class through one or several constructions
use
inside the class definition. use
can be specified anywhere in the class. Additionally, in block (
{...}
) after use
you can:- assign alias to the trait methods ( - from Trait will be additionally available as );
Trait::method as myMethod
method
myMethod
- indicate the overlap of the method of one type, the method of another, if they have the same name ( - the method will be used instead of the method of the same name );
TraitA::method insteadof TraitB
TraitA
TraitB
- to increase or decrease access to a method from a trait, with the exception of translating the method into static ( ), you can immediately rename ( ).
Trait::publicMethod as protected
Trait::publicMethod as protected _myProtectedMethod
The type itself is recorded, as
trait
it may include other types, by indicating them in the keyword use
. The syntax and capabilities are similar use
in class. More complex example:
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
It is important to pay attention to two points. Firstly, the block after
use
seems related to the type near which it is described, but this is not so. The rules in the block are global and can be declared anywhere. To avoid confusion, it is a good practice to write all traits first, separated by commas, and then overlap rules and alias on a separate line. Or describe all the rules for the type next to its connection. The choice is yours.
//так
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;
}
Secondly, pay attention to the list of methods, the list remains
getMyNickname
, but _getMyLogin
just its alias with reduced access. You can exclude the original method altogether, but more on that later in the magic section. Traits are initialized, like classes, dynamically. With a strong desire, you can write like this:
if ($isWin) {
trait A { /* … */}
} else {
trait A { /* … */}
}
Traits properties
Before that, I operated on methods, but the type may include properties that will be added to the class. In this regard, “traits” in php are more like mixin.
trait WithId
{
protected $_id = null;
public function getId()
{
return $this->_id;
}
public function setId($id)
{
$this->_id = $id;
}
}
Immediately I suggest good practice, so that one day it does not turn out that the property
_id
in the type conflicts with that used in the class or its descendants, write the properties of the types with prefixes: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;
}
}
Area of visibility
Важно понимать, как будут разрешаться различные вызовы внутри типажа. В этом поможет правило думать о подключении типажа, как о «copy-paste» кода в целевой класс. В самом первом примере, интерпретатор как бы сделал «copy-paste» метода
whoAmI
в класс Human
, соответственно все вызовы к parent
, self
, $this
будут работать также, как и вызов в методах класса. Исключение будут составлять некоторые магические константы, например внутри whoAmI
__METHOD__ === 'Pprint::whoAmI'.Внутри методов типажа доступны все свойства объекта для обращения напрямую, никаких дополнительных областей видимости не добавляется. Можно было бы получить просто
$this->_name
, вместо вызова __toString
. However, it’s worth thinking a few times before doing this, since on complex implementations this will bring quite a bit of confusion. I would recommend that you always use clear methods, if necessary, even describe them in the interface and “force” the classes to implement it.Static Methods and Properties
You can declare static methods in a type, but you cannot declare static properties. Inside static methods, you can use both static binding (self: :) and dynamic binding (static: :), everything will work as if called from a class method (“copy-paste”).
The restriction on the storage of static properties can be circumvented, as I will show later with an appeal to magic.
The coincidence of the methods of types between themselves and with the methods of the class
The method described in the class overrides the method from the type. But if some method is described in the parent class, and a type with the same method is connected in the child class, it will override the method from the parent (again, remember “copy-paste”).
If several methods specified by the class use the same methods, php will throw an error at the initialization stage of the class:
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%
He comes to the rescue insteadof
, with the help of which it will be necessary to resolve all conflicts. A tricky mistake can be when the method that caused the collision is also defined in the class, in which case php will skip this check, because he checks only the "surviving" trait methods:
trait A
{
public function abc() {}
}
trait B
{
public function abc() {}
}
class C
{
use A, B;
public function abc() {}
}
//OK
Sometime later, transferring the method abc
to the parent class, we get a strange error in the collision of trait methods, which can be confusing. So collisions are best resolved in advance. ( On the other hand, if the methods of the type and the class are the same in the code, something is probably wrong. )Match type properties with properties of another type and class properties
At this moment, unpleasant problems await us. Immediately an example:
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%
I explain. In the general case, when intersecting the properties of the types between themselves or the properties of the type and the class, an error is thrown. But for some reason an exception is made for "compatible" properties and they work on the principle of "who is last, that is right." Therefore, in the class
A
in getId
it turned out NULL, and in the class B
- false. In this case, the properties of the class are considered lower than the property of the type (with methods it is the opposite) and C
instead of the expected '0' we get false. Values that are considered compatible are non-strict comparisons of which give true, and since php has a lot of implicit conversions, there can be unpleasant errors when using strictly comparisons of returned values.
var_dump(null == false); //true
var_dump('0' == false); //true
var_dump('a' == null); //false
So the practice with prefixes proposed above will be useful in such cases. I hope that this part of the implementation will be revised for release.
Errors and exceptions in traits
If you follow the mnemonic rule trait == “copy-paste”, everything becomes immediately clear with errors:
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}
The object no longer knows where it came from in which there was a Notice or Exception, but this can be found in the stack trace by the lines of code in which the calls were. If you store traits in separate files, it will be even easier to determine.
A bit of white black magic
I will show a couple of dirty tricks with types, use them at your own peril and risk.
Deleting a trait method
To remove a trait method, for example, when it was given alias, you can do this:
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
But in this approach, there is a great danger, because some type methods can potentially call other methods:
$c = new C();
$c->d();
//Fatal error: Call to undefined method C::e()
When renaming, the type does not know that the method has been renamed. Therefore, by default, when specifying alias, the original method is saved.
"Inheritance" in types
Using a similar trick, you can implement “inheritance” in traits with the ability to call “parent” methods.
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_();
}
}
Two ways to implement Singleton with traits
To smooth out this magical disgrace I will show one useful example. Often, Singleton is given in the form of a type, although without the possibility of setting a static variable in the type, making it will not be as simple as it seems at first glance. You can use two tricks.
The first is to get the name of the class to which it was called inside the called method, and then use a separate class with a static method as storage, something like this:
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);
}
}
The second is to use feature roofing felts, php roofing felts, which is associated with the use of a keyword
static
when declaring a variable. These variables must retain their value when calling the method, but apparently the structure for storing these variables is initialized at each place the method is used. The result is this scheme: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