This idea is a continuation of the stateful traits discussed in “Stateful Traits and their Formalization” [1]. I would suggest reading Section 4: “Stateful traits: reconciling traits and state” before continuing this discussion.
Instead of automatically including all trait methods into the scope of the class, all methods and properties are automatically in the local scope of the trait. The developer then has the option of finer grained control on the method and property level.
The options include the following:
For all of the code examples, the following keywords were used:
trait
: This keyword is used to mark a trait definition exactly as the class
keyword works for classes.
class
: Same as current PHP keyword.
include
: Used to include a trait into a class definition
private
: This keyword is used in its current PHP sense as well as to mean that a property or method is to retain its local trait scope.
public
: This keyword is used in its current PHP sense as well as to mean that a property or method is to be used within the scope of the including class.
as
: Used to alias or merge a trait into a class.
Keeping a method or property local to a trait denies access from within the class or an object created from the class. It also keeps all references from methods within that trait to the method or property linked to the local method, even if the same name is used in another included trait, or is overridden in the including class.
The problem with including an entire trait locally is that there is no way to use the methods or properties. The trait is only useful if you include at least one of the methods or properties into the class scope.
The following code example shows how local scoping works::
trait A1 { private $a = 'A1'; private function printA() { echo $this->a; } public function callPrintA1() { $this->printA(); } } trait A2 { private $a = 'A2'; protected function printA() { echo $this->a; } public function callPrintA2() { $this->printA(); } } class A { include A1 { public callPrintA1(); } include A2 { public printA(); } public function callPrintA() { $this->printA(); } } $a = new A(); $a->callPrintA1(); // 'A1' $a->callPrintA2(); // Fatal Error: Undefined method... $a->callPrintA(); // 'A2'
When placed into the scope of the class, a method or property behave as if it were copied and pasted directly into the class, with one exception: to not break the rules of trait local scope, any method or property with trait local scope in the class is used instead of a property or method in the class scope. This exception is the most important distinction between normal traits and non-breaking traits.
As shown in the code example for local scope, the method call to
callPrintA1
has a subsequent call to printA
. Since the printA
method from the trait was in the local scope, it is called instead of
the method in the class scope.
An alias can be made of a method or property in either local or class scope. When the alias is made, it is placed into the class scope (what else would the alias be for?). The alias still adheres to the rules for methods and properties locally scoped to the trait. Also, any change to an aliased property changes the original property. The same is not true for an aliased method being reimplemented in the class; it will not replace a trait's local method.
The following code example shows how aliasing works:
trait A1 { private $a = 'A1'; private function printA() { echo $this->a; } public function callPrintA1() { $this->printA(); } } trait A2 { private $a = 'A2'; protected function printA() { echo $this->a; } public function callPrintA2() { $this->printA(); } } class A { include A1 { private $a as $a_from_A1; public callPrintA1(); } include A2 { public printA(); private callPrintA2() as callPrintA(); } public function setA1( $value ) { $this->a_from_A1 = $value; } } $a = new A(); $a->callPrintA1(); // 'A1' $a->setA1( 'A1-changed' ); $a->callPrintA1(); // 'A1-changed' $a->callPrintA2(); // Fatal Error: Undefined method... $a->callPrintA(); // 'A2'
Merging is an extension of aliasing. Instead of aliasing properties or methods to unique names, multiple properties or methods are aliased to the same name, and an implementation of the method or a value for the property is placed in the combined class.
To access the original method from within the class, multiple aliases will need to be made, or the method can be made local to the class scope and accessed from its original name.
The following code example shows how aliasing works:
trait A1 { private $a = 'A1'; private function printA() { echo $this->a; } public function callPrintA1() { $this->printA(); } } trait A2 { private $a = 'A2'; protected function printA() { echo $this->a; } public function callPrintA2() { $this->printA(); } } class A { include A1 { private $a as $my_a; public callPrintA1() as callPrintA(); } include A2 { private $a as $my_a; public printA(); public callPrintA2() as callPrintA(); } private $my_a = 'A'; public function setA( $value ) { $this->my_a = $value; } } $a = new A(); $a->callPrintA1(); // 'A' $a->setA( 'A-changed' ); $a->callPrintA1(); // 'A-changed' $a->callPrintA2(); // 'A-changed' $a->callPrintA(); // 'A-changed'
Two approaches to implementation are outlined below:
I am not familiar with PHP internals, and these are only my thoughts on the matter. I will let others decide the best route to take on this.
The idea here is to place all of the properties and methods into the class definition at compile time. This makes it possible to run the code as if traits don't even exist. The benefits of this is that the run-time internals of PHP should not be affected. The downside is that compiling becomes more complicated.
To keep methods and properties from colliding, some kind of alpha-renaming of those that are locally scoped must be made. The most common suggestion is to append the trait name (including namespace) to the front of the method or property seperated by the double colon (::). This should be an acceptable solution to the issue. However, every use of the method or property within the trait, must be changed to this new name.
Instead of flattening, the traits could be kept separate from the class. Instead, during runtime, when a call is made to a trait method or property, the correct action is decided. This will require changes to the run-time internals of PHP, but could provide some benefit to opcode caches, as they could cache the trait once and use it for each of the classes that include it.
An outline of non-breaking traits has been given. The changes build upon the work on stateful traits, and take it to the next obvious step. The benefit of this approach over multiple inheritance or mix-ins, is that the developer of the class has full control over exactly how the traits will be included into the class.
Only simple code examples have been given, and questions will surely arise over what will happen if you include traits a certain way. In this case, more explanation and example code may be required.
[1] Alexandre Bergel, Stéphane Ducasse, Oscar Nierstrasz and Roel Wuyts,
“Stateful Traits and their Formalization,” Journal of Computer
Languages, Systems and Structures, vol. 34, no. 2-3, 2008, pp. 83-108.
http://www.iam.unibe.ch/~scg/Archive/Papers/Berg07eStatefulTraits.pdf