9. Traits
Traits are structures analogous to classes. However, they cannot be instantiated: they are intended to be included in classes. Including a trait in a class has the same effect as if the trait’s code had been copied into the class. Code that can be reused in multiple classes is placed in a trait.
Traits have been available since PHP 5.4. PHP 8 introduced several new features, which we will discuss at the end of this chapter: signature checking for abstract methods (PHP 8.0), trait constants (PHP 8.2), the [#[\Override]] attribute usable in a trait (PHP 8.3).
9.1. The script directory structure
The scripts in this chapter are located in the [scripts-console/traits] folder:
9.2. Including a trait in a class
The script [trait-01.php] demonstrates a basic use of a trait in a class:
Code comments
- lines 5–22: definition of the [Class04] class with an attribute [$name] (initialized by the constructor), its getter, and its setter;
- lines 24–39: We take the code from [Class04]—excluding its constructor—and move it into the trait [Trait04]. This time, the attribute is explicitly declared, along with its type. We do not include the constructor since a trait cannot be instantiated;
- line 24: the keyword [trait] designates [Trait04] as a trait rather than a class;
- lines 41–51: We define a class [Class05] that includes the code from the trait [Trait04] and adds a constructor to it, to make the class instantiable;
- Line 44: The keyword [use], placed inside a class, allows a trait to be included in a class. It should not be confused with the statement [use], which is placed outside any class and imports a name from a namespace;
- Lines 54–60: Tests show that the classes [Class04] and [Class05] function in the same way.
Results
The results in lines 4–11 show that the objects [Class04] and [Class05] have the same content: the private attribute [$name] of the trait has become a private attribute of the class [Class05].
Conclusion: Using the [use Trait] statement in a class is equivalent to including the code from [Trait] in the class.
9.3. Using the Same Trait in Different Classes
One of the main benefits of a trait seems to be the reuse of the same code (attributes + methods) across different classes. However, we will see that the same goal can be achieved using simple classes.
Sharing a trait across classes is illustrated by the following script, [trait-02.php]:
Comments
- lines 5–15: a trait defining an attribute [$id] and a method [doSomething];
- The trait [Trait01] is included in two classes: [Class02] (lines 17–36) and [Class03] (lines 38–53);
- lines 20–24: inclusion of [Trait01] in [Class02]. The inclusion is followed by a [{...}] block that allows the included methods to be adjusted;
- line 23: This line resolves a conflict: [Trait01] and [Class02] both have a method named [doSomething]. In this case, the class method takes precedence: the trait method is hidden. The [as] operator makes the [Trait01::doSomething] method accessible in the class under the name [doSomethingInTrait]. Thus, within [Class02], the following notations will be used:
- [doSomethingInTrait] to call the method [Trait01::doSomething];
- [doSomething] to call the method [Class02::doSomething];
- Lines 30–32: The class [Class02] uses the attribute and method of [Trait01] as if they were its own;
- lines 38–53: the class [Class03] is analogous to the class [Class02]. The inclusion of [Trait01] is simpler here because there is no conflict between the methods of [Trait01] and [Class03].
Results
Note that the trait [Trait01] is not shared between the classes [Class02] and [Class03]. The attribute [$id] of [Trait01] becomes, due to the inclusion of [Trait01] in the classes [Class02] and [Class03], two distinct attributes: [Class02::$id] and [Class03::$id]. This is shown in lines 3 and 5 of the results. If the attribute [$id] had been shared between the classes [Class02] and [Class03], the value on line 5 would have been 20 instead of 10.
The script [trait-03.php] shows that the same result can be achieved by using a class instead of the trait:
Comments
- Lines 6–28: The class [Class01] replaces the trait [Trait01]. The code for [Trait01] was incorporated into the code for the classes [Class02] and [Class03]. Here, that will not be the case: a reference to a [Class01] object will be injected into the [Class02] and [Class03] classes. The attributes of the [Class01] class will therefore be external to the [Class02] and [Class03] objects. Since the [$id] attribute is private, a getter and a setter must be provided. This is the first difference from a trait: you must create code to access the class’s private attributes. We could have made the [$id] attribute public, but this is not recommended: using a setter to set an attribute’s value allows you to validate it;
- Line 33: The reference to the object
*[Class01]is injected into*[Class02]by its constructor, thanks to parameter promotion. The PHP 7 course used a setter [setClass01]. Injection by the constructor is preferable: the [Class02] object is complete as soon as it is constructed, and since the attribute is [readonly], the reference can no longer be changed; - lines 41–45: for any use of the code [Class01], we must go through the attribute [$this->class01];
- lines 51–70: The class [Class03] is a clone of the class [Class02], except that its method has a different name;
- lines 73–81: the first test. This involves injecting two different instances of the class [Class01] into the objects [Class02] and [Class03];
- lines 84–94: The second test injects the same instance of the [Class01] class into the objects [Class02] and [Class03].
Results
Comments on the results
- lines 3–6: We obtain the same results as with the trait [Trait01]. We can conclude that using the trait is not essential here, but it does reduce the amount of code because there is no need for methods to access the trait’s attributes: these are an integral part of the class into which the trait has been incorporated;
- Lines 8–11: Because the same reference to [Class01] was injected into the objects [Class02] and [Class03], the attribute [Class01::$id] was shared between the two objects. This is why line 10 of the results displays 20 instead of 10 when using the trait. We can conclude that if attributes need to be shared between objects, the trait cannot be used, and a class must be used instead.
9.4. Grouping Methods in a Trait
In the previous example, the trait included attributes and methods. Here, we’ll consider the case where it contains only methods. The trait then resembles a factorization of methods that can be used in different classes. Since the trait has no attributes here, we’ll consider the case where the methods it contains operate solely on the parameters passed to them. This is not mandatory: a trait can operate on an attribute [$this->attribute1] without having that attribute itself. In that case, it is up to the classes that use this trait to provide the attribute [$this->attribute1].
In the case where the trait has only methods that operate solely on parameters passed to them, we will show that the trait can be replaced by a class with the same methods as the trait, declared as static.
The use of the trait is illustrated by the script [trait-04.php], which reuses the trait from the previous example but removes all attributes:
Comments
- lines 5–12: the trait [Trait01] no longer has any attributes;
- lines 17–21: [Trait01] is included in [Class02]. The method of [Trait01] is used on line 26;
- line 35: [Trait01] is included in [Class03]. The method from [Trait01] is used on line 41.
Results
In this use case, the trait [Trait01] can easily be replaced by a class. This is demonstrated by the following [trait-05.php] script:
Comments
- Lines 6–18: The trait [Trait01] is replaced by a class [Class01], all of whose methods are static. There is no need to instantiate or subclass it. The PHP 7 course declared it abstract to prevent its instantiation, regretting that PHP rejects the prefix [final abstract]. This is still the case in PHP 8: instead, we declare the class [final] (non-derivable) with a private constructor (non-instantiable). This is what we did for the class [Utilities] in version 3 of the application exercise;
- line 25: instead of writing [$this->doSomethingInTrait()], we now write [Class01::doSomething()], meaning we call the static method [doSomething] of the class [Class01];
- line 37: repeat the same process in [Class03].
Results
We indeed get the same result as with the trait [Trait01], which shows that its use can be avoided. We noted that a trait’s methods can operate on an attribute [$this->attribute1] even if the trait itself does not have that attribute; it is up to the classes that use the trait to provide it. This is an exotic and fragile scenario: it is better to “pass up” the attribute [$this->attribute1] to the trait itself. That way, it will necessarily be part of the attributes of the class using the trait. We’ll see at the end of the chapter that PHP 8 offers a clean solution for requiring something from classes that use a trait: abstract methods.
9.5. Multiple Inheritance with a Trait
It is common to read in the literature that traits allow for multiple inheritance: the ability for a class to inherit from multiple classes. The C++ language supports this feature, but neither Java nor C# do, as they only support single inheritance. We will show that while using a trait in a derived class does indeed allow you to implement something resembling multiple inheritance, this use case can also be implemented using simple classes.
The script [trait-06.php] implements a derived class and a trait:
Comments
- lines 5–18: a trait [Trait01] with an attribute and a method that manipulates it;
- lines 20–33: a class [Class02] that has nothing to do with the trait [Trait01]. It does not use it;
- line 23: the sole attribute of [Class02] has the visibility [protected] so that it is accessible in derived classes;
- Line 36: The class [Class03] extends the class [Class02]. In addition, it incorporates the trait [Trait01] (line 39). Finally, it inherits the attributes and methods of [Class02] and incorporates the attributes and methods of [Trait01]. This is therefore analogous to multiple inheritance.
Results
Just as in a previous example, we will show that:
- the trait can be replaced by a class;
- instead of incorporating the trait into the derived class, we inject a reference to an instance of that class into it.
The script [trait-07.php] is as follows:
Comments
- Lines 5–29: The class
*[Class01]replaces the trait*[Trait01]. Since a [Class01] object will be used via a reference, a getter and a setter have been provided for the [$i] attribute; - lines 31–44: The [Class02] class remains unchanged;
- line 46: the [Class03] class extends the [Class02] class;
- line 49: a reference to an object [Class01] is injected into [Class03] by its constructor;
- Lines 54–66: The [doSomethingInClass03] method does the same thing as before, but with more complex code.
Results
From this example, we can conclude that, once again, the trait is not essential, but it must be acknowledged that it allows for shorter code in the derived class.
9.6. Using a trait instead of an abstract class
The following use case is common: you create a fairly general interface I that can give rise to multiple implementations. These implementations share common code but differ in other methods. There are two ways to implement this use case:
- You create an abstract class
Cthat contains the code common to the derived classes. ClassCimplements interfaceI, but certain methods—which must be defined in the derived classes—are declared abstract in classC, which is therefore itself abstract. We then create classes C1 and C2 derived from C, each of which implements the abstract methods of their parent class C in its own way; - we create a trait T that is nearly identical to the abstract class C from the previous solution. This trait does not implement the interface I because, syntactically, it cannot. We then create the classes C1 and C2, which implement the interface I and use the trait T. All that remains for these classes to do is to implement the methods of the interface I that are not implemented by the trait T that they import.
Here is an example that illustrates how similar these two solutions are. Application 1 implements Solution 1 ([trait-08.php]):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | |
Comments
- lines 5–10: The interface [Interface1] has two methods;
- line 12: The abstract class [AbstractClass] implements the interface [Interface1]. It has two attributes with their getters and setters, implements the [doSomething] method of the [Interface1] interface (line 43), but does not implement the [doSomethingElse] method. This method is therefore declared abstract (line 50). The abstract class [AbstractClass] cannot be instantiated and, to be useful, must be derived from;
- Lines 29–39: The setters are “fluent”: instead of returning nothing, they return the object [$this]. This allows for chained calls (line 116). The return type [static] (PHP 8.0) refers to the class of the object on which the method was called: [(new Class1())->setAttr1(21)] is of type [Class1] and not [AbstractClass], which allows [setAttr3]—a method specific to [Class1]—to be called next;
- Line 54: The class [Class1] extends the abstract class [AbstractClass] and therefore implements the interface [Interface1]. It provides a body for the method [doSomethingElse], which its parent class had not defined. It also adds an attribute to those of its parent class;
- Line 80: The class [Class2] does the same with another attribute;
- lines 106–110: The function [useInterfaceWith] receives a type [Interface1] as a parameter and calls both methods of this interface;
- Lines 113–116: The function [useInterfaceWith] is called with a type [Class1] and then with a type [Class2]. This is correct because both of these types implement the [Interface1] interface.
Results
Now we implement Solution 2 using the script [trait-09.php]. This involves replacing the abstract class with a trait:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | |
Comments
- lines 5–10: the [Interface1] interface has not changed;
- lines 12–47: The trait [Trait1] replaces the abstract class [AbstractClass] from Solution 1. The code is the same except for the following details:
- line 12: the trait [Trait1] does not implement the interface [Interface1]. This is syntactically impossible;
- lines 15–16: the visibility of the attributes of the abstract class
*[AbstractClass]changes here from***[protected]to[private]***. In both cases, the goal is to give concrete classes direct access to the attributes without going through getters and setters. With a trait, [private]* is sufficient since the trait’s attributes become those of the class; - the trait [Trait1] does not declare the abstract method [doSomethingElse];
- Line 42: Starting with PHP 8.3, the attribute [#[\Override]] can be applied to a trait method. This is checked in every class that uses the trait: here, the method [doSomething] must correspond to a method of an interface or a parent class of [Class1] and [Class2]. This is indeed the case: it is the method [doSomething] of [Interface1];
- Line 50: The class
*[Class1]in Solution 2 is identical to the class*[Class1]in Solution 1, except for the following details:- it implements the [Interface1] interface, whereas in Solution 1, it extended the abstract class [AbstractClass];
- it uses the trait [Trait1] (line 53) to implement part of the interface;
- Line 79: The same comments apply as for [Class1];
- the rest of the code remains unchanged.
Results
We do indeed get the same results.
9.7. Traits in PHP 8
PHP 8 has made traits more reliable and complete. The script [trait-10.php] illustrates these new features:
Comments
- line 9: Since PHP 8.2, a trait can declare constants. These become constants of the class that uses the trait;
- line 12: A trait can declare static attributes. Like instance attributes, they are copied into each class that uses the trait: each class has its own instance counter;
- Lines 18–21: A trait can declare static methods. In the trait’s code, [self] refers to the class that uses the trait;
- Line 24: A trait can declare abstract methods. Any class that uses the trait must implement them; otherwise, it must itself be declared abstract (see the class [Plane] commented out at the end of the script). Starting with PHP 8.0, the implementation signature (parameter and return types) is verified. This is the proper way to make requirements of the classes that use the trait, rather than assuming they have a particular attribute or method;
- Lines 33–38: A concrete method [increment] that uses the abstract method [getName] and the constant [STEP]. It returns [$this] (type [static]) so that it can be chained;
- Lines 41–56: The class [Car] uses the trait, calls the trait’s private method [register] in its constructor, and implements the abstract method [getName]. Note the [#[\Override]] attribute on this implementation: it is accepted because the class implements an abstract method of the trait;
- lines 58–72: the class
*[Bike]* does the same; - lines 79–80: Each class has its own static attribute [$nbInstances];
- line 82: the trait’s constant is accessible via the class that uses it;
- line 84: but not directly via the trait: an error occurs.
Results
9.8. Conclusion
From the previous examples, it is clear that use cases where using a trait provides a net benefit are not obvious. In our examples, we can always do without it by replacing it with a class. However, it is useful for factoring out code across different classes, as if that code belonged to a parent class, without consuming the single inheritance available to a PHP class. This is what we will do in version 4 of the application exercise.