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 :
//определение типажа
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 Humantype 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 useinside the class definition. usecan be specified anywhere in the class.

Additionally, in block ( {...}) after useyou can:
  • assign alias to the trait methods ( - from Trait will be additionally available as );Trait::method as myMethodmethodmyMethod
  • 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 TraitBTraitATraitB
  • 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 protectedTrait::publicMethod as protected _myProtectedMethod

The type itself is recorded, as traitit may include other types, by indicating them in the keyword use. The syntax and capabilities are similar usein 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 useseems 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 _getMyLoginjust 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 _idin 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 abcto 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 Ain getIdit 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 Cinstead 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 staticwhen 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


PS

Thanks aveic for the help and interesting ideas on working with traits.

Also popular now: