📜 ⬆️ ⬇️

Traits in php 5.4. Parse implementation details

Most recently , the first beta php 5.4 was released , but for now I wrote a topic and the second one arrived . One of the innovations in 5.4 is traits (types). I propose to understand all the details of what types are of themselves in php.

A simple example of a type, so as not to look at 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 behavior from the Pprint type has been added to the Human class.

But everything has its own details.
')

Syntax


In general, everything is simple. You can connect types to an unlimited number of classes through one or more use constructs within the class definition. use can be specified anywhere in the class.

Additionally in the block ( {...} ) after use you can:

The type itself is recorded as a trait and may include other types, by specifying them in the use keyword. The syntax and capabilities are similar to use in a class.

More complicated 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('/[^az]+/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. First, the block after use seems to be associated with the type around which it is described, but it is not. The rules in the block are global and can be declared anywhere.

In order to avoid confusion, it will be good practice to write down all types first, separated by commas, and then on a separate line, overlap rules and alias. 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, getMyNickname remained in the list, and _getMyLogin just its alias with reduced access. You can eliminate the original method at all, but more on that below in the section of magic.

Types are initialized, like classes, dynamically. With a great desire, you can write like this:
 if ($isWin) { trait A { /* … */} } else { trait A { /* … */} } 

Properties in types


Before that, I operated on methods, but the type may include properties that will be added to the class. In this regard, "types" in php - it is rather a mixin.
 trait WithId { protected $_id = null; public function getId() { return $this->_id; } public function setId($id) { $this->_id = $id; } } 

Immediately I propose a good practice, so that once it does not turn out that the _id property in the type conflict with that used in the class or its descendants, the properties of the types write with the 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


It is important to understand how the various calls within the type will be resolved. This will help the rule to think about connecting the type as “copy-paste” code into the target class. In the very first example, the interpreter would have made the “copy-paste” of the whoAmI method into the Human class, respectively, all calls to parent , self , $this will work as well as the call in the class methods. The exception will be some magic constants, for example inside whoAmI __METHOD__ === 'Pprint :: whoAmI'.

Inside the type methods, all properties of the object are accessible for direct access, no additional scopes are added. One would get just $this->_name , instead of calling __toString . However, it is worth thinking a few times before doing this, since on complex implementations this will cause quite a bit of confusion. I would recommend always using clear methods, if necessary, even describe them in the interface and "force" classes to implement it.

Static methods and properties


In a typesetter, you can declare static methods, but you cannot declare static properties. Inside static methods, you can use both static binding (self: :) and dynamic (static: :), everything will work as if caused from the class method ("copy-paste").

Restriction on the storage of static properties can be circumvented, as it will show later with an appeal to magic.

The coincidence of methods of types between themselves and with the methods of the class


The method described in the class overrides the type of method. But if a method is described in the parent class, and a type is connected in the child class with the same method, it will override the method from the parent class (again, recall “copy-paste”).

If several methods specified by the class use the same methods, php will generate an error at the class initialization stage:
 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% 
To the rescue comes insteadof , through which you will need to resolve all conflicts.

A tricky mistake can be in the case when the class also has a method that caused a collision, in which case php will skip this check, since he checks only the “surviving” type methods:
 trait A { public function abc() {} } trait B { public function abc() {} } class C { use A, B; public function abc() {} } //OK 
Sometime later, by transferring the abc method to the parent class, we get a strange error on the collision of type methods, which can be confusing. So, it is better to resolve conflicts in advance. ( On the other hand, if the type and class methods in the code are the same, something is probably wrong. )

The match of the properties of the type with the properties of another type and properties of the class


In this moment we face unpleasant problems. Just 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 crossing the properties of the types between themselves or the properties of the type and class, an error is generated. But for some reason for the "compatible" properties an exception is made and they work according to the principle "who is the last, that is right". Therefore, in class A , getId was NULL, and in class B , false. In this case, class properties are considered lower than the type property (with methods equal to the opposite) and in C instead of the expected '0', we get false.

Compatible values ​​are considered to be non-strict comparison of which gives true, and since in php there are many implicit conversions, there can be unpleasant errors when using strictly comparing returned values.
 var_dump(null == false); //true var_dump('0' == false); //true var_dump('a' == null); //false 

So the practice with prefixes suggested above will be useful in such cases. I hope that this part of the implementation will be revised to the release.

Errors and exceptions in types


If you follow the mnemonic rule trait == "copy-paste", everything becomes immediately clear with errors:
 <?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} 

The object no longer knows where it came from the method in which there was a Notice or Exception, but you can find out in the stack trace by the lines of code in which there were calls. If you store types in separate files, it will be even easier to determine.

Little white black magic


I will show a couple of dirty tricks with types, use them at your own peril and risk.

Type removal method


To remove the type method, for example, when an alias was given to it, 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 lies a great danger, because some methods of type can potentially call other methods:
 $c = new C(); $c->d(); //Fatal error: Call to undefined method C::e() 

When renaming a type, it does not know that the method has been renamed. Therefore, by default, if you specify alias, the original method is preserved.

"Inheritance" in types


Using a similar trick, you can implement "inheritance" in types 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 types


To smooth out this magical disgrace I will show one useful example. Often, a Singleton is given as a type, although without the possibility of setting a static variable in a type, it would not be as easy to make it as it seems at first glance. You can use two tricks.

The first is to get inside the called method the name of the class to which it was called, and then use a separate class with a static method as storage, 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 toli features, toli php bug, which is associated with the use of the static when declaring a variable. These variables should retain their value when the method is called, but apparently the structure for storing these variables is initialized at each place where the method is used. The result is such a 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

Thank you aveic for help and interesting ideas on working with types.

Source: https://habr.com/ru/post/130000/


All Articles