Skip to content

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:

1
2
3
4
5
6
php8-oct-2026/scripts-console/traits
├── trait-01.php
├── trait-02.php
├── ...
├── trait-09.php
└── trait-10.php

9.2. Including a trait in a class

The script [trait-01.php] demonstrates a basic use of a trait in a class:

<?php

declare(strict_types=1);

class Class04
{
    // constructor with promotion of the [name] attribute
    public function __construct(private string $name)
    {
    }

    // getter and setter
    public function getName(): string
    {
        return $this->name;
    }

    public function setName(string $name): void
    {
        $this->name = $name;
    }
}

trait Trait04
{
    // attribute
    private string $name;

    // getter and setter
    public function getName(): string
    {
        return $this->name;
    }

    public function setName(string $name): void
    {
        $this->name = $name;
    }
}

class Class05
{
    // inclusion of the trait
    use Trait04;

    // constructor
    public function __construct(string $name)
    {
        $this->name = $name;
    }
}

// test --------------
$class04 = new Class04("Tim");
$class05 = new Class05("Burton");
print $class04->getName() . "\n";
print $class05->getName() . "\n";
// display of the two objects
print_r($class04);
print_r($class05);

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

> php trait-01.php
Tim
Burton
Class04 Object
(
    [name:Class04:private] => Tim
)
Class05 Object
(
    [name:Class05:private] => Burton
)

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]:

<?php

declare(strict_types=1);

trait Trait01
{
    // attribute
    private int $id = 0;

    // method
    public function doSomething(): void
    {
        print "Trait01::doSomething... ($this->id)\n";
    }
}

class Class02
{
    // inclusion of Trait01
    use Trait01 {
        // the method [Trait01::doSomething] is accessible
        // in the class under the name [doSomethingInTrait]
        Trait01::doSomething as doSomethingInTrait;
    }

    // method specific to the class
    public function doSomething(): void
    {
        // id attribute
        $this->id += 10;
        // use of the method of Trait01
        $this->doSomethingInTrait();
        // local display
        print "Class02->doSomething\n";
    }
}

class Class03
{
    // inclusion of Trait01
    use Trait01;

    // method local to the class
    public function doSomethingElse(): void
    {
        // id attribute
        $this->id += 10;
        // use of the method of Trait01
        $this->doSomething();
        // local display
        print "Class03->doSomethingElse\n";
    }
}

// test01 ----------------
function test01(): void
{
    $class02 = new Class02();
    $class03 = new Class03();
    $class02->doSomething();
    $class03->doSomethingElse();
}

// test01
print "test01-----------------\n";
test01();

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

1
2
3
4
5
6
> php trait-02.php
test01-----------------
Trait01::doSomething... (10)
Class02->doSomething
Trait01::doSomething... (10)
Class03->doSomethingElse

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:

<?php

declare(strict_types=1);

// class that replaces the trait
class Class01
{
    // attribute
    private int $id = 0;

    // setter
    public function setId(int $id): void
    {
        $this->id = $id;
    }

    // getter
    public function getId(): int
    {
        return $this->id;
    }

    // method
    public function doSomething(): void
    {
        print "Class01::doSomething... ($this->id)\n";
    }
}

class Class02
{
    // a reference to an instance of Class01, injected by the constructor
    public function __construct(private readonly Class01 $class01)
    {
    }

    // method specific to the class
    public function doSomething(): void
    {
        // modification of the attribute of Class01
        $id = $this->class01->getId();
        $id += 10;
        $this->class01->setId($id);
        // use of the method of Class01
        $this->class01->doSomething();
        // local display
        print "Class02->doSomething\n";
    }
}

class Class03
{
    // a reference to an instance of Class01, injected by the constructor
    public function __construct(private readonly Class01 $class01)
    {
    }

    // method local to the class
    public function doSomethingElse(): void
    {
        // modification of the attribute of Class01
        $id = $this->class01->getId();
        $id += 10;
        $this->class01->setId($id);
        // use of the method of Class01
        $this->class01->doSomething();
        // local display
        print "Class03->doSomethingElse\n";
    }
}

// test01 ----------------
function test01(): void
{
    // two objects that will access two different instances of [Class01]
    $class02 = new Class02(new Class01());
    $class03 = new Class03(new Class01());
    // check
    $class02->doSomething();
    $class03->doSomethingElse();
}

// test02 ----------------
function test02(): void
{
    // shared instance of [Class01]
    $class01 = new Class01();
    // two objects that will access the same instance of [Class01]
    $class02 = new Class02($class01);
    $class03 = new Class03($class01);
    // check
    $class02->doSomething();
    $class03->doSomethingElse();
}

// test01
print "test01-----------------\n";
test01();
// test02
print "test02-----------------\n";
test02();

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

> php trait-03.php
test01-----------------
Class01::doSomething... (10)
Class02->doSomething
Class01::doSomething... (10)
Class03->doSomethingElse
test02-----------------
Class01::doSomething... (10)
Class02->doSomething
Class01::doSomething... (20)
Class03->doSomethingElse

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:

<?php

declare(strict_types=1);

trait Trait01
{
    // method to be shared
    public function doSomething(): void
    {
        print "Trait01::doSomething...\n";
    }
}

class Class02
{
    // inclusion of Trait01
    use Trait01 {
        // the method [Trait01::doSomething] is accessible
        // in the class under the name [doSomethingInTrait]
        Trait01::doSomething as doSomethingInTrait;
    }

    public function doSomething(): void
    {
        // call of the method of Trait01
        $this->doSomethingInTrait();
        // local display
        print "Class02->doSomething\n";
    }
}

class Class03
{
    // inclusion of Trait01
    use Trait01;

    // method local to the class
    public function doSomethingElse(): void
    {
        // call of the method of Trait01
        $this->doSomething();
        // local display
        print "Class03->doSomethingElse\n";
    }
}

// test ----------------
(new Class02())->doSomething();
(new Class03())->doSomethingElse();

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

1
2
3
4
5
> php trait-04.php
Trait01::doSomething...
Class02->doSomething
Trait01::doSomething...
Class03->doSomethingElse

In this use case, the trait [Trait01] can easily be replaced by a class. This is demonstrated by the following [trait-05.php] script:

<?php

declare(strict_types=1);

// class neither instantiable nor derivable, containing only static methods
final class Class01
{
    // private constructor: no instantiation possible outside the class
    private function __construct()
    {
    }

    // static method to be shared
    public static function doSomething(): void
    {
        print "Class01::doSomething...\n";
    }
}

class Class02
{
    public function doSomething(): void
    {
        // call of the static method of Class01
        Class01::doSomething();
        // local display
        print "Class02->doSomething\n";
    }
}

class Class03
{
    // method local to the class
    public function doSomethingElse(): void
    {
        // call of the static method of Class01
        Class01::doSomething();
        // local display
        print "Class03->doSomethingElse\n";
    }
}

// test ----------------
(new Class02())->doSomething();
(new Class03())->doSomethingElse();

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

1
2
3
4
5
> php trait-05.php
Class01::doSomething...
Class02->doSomething
Class01::doSomething...
Class03->doSomethingElse

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:

<?php

declare(strict_types=1);

trait Trait01
{
    // attribute
    private int $i = 0;

    // method to be shared
    public function doSomethingInTrait01(): void
    {
        // modification of Trait01::$i
        $this->i++;
        // display
        print "Trait01::doSomethingInTrait01... i=$this->i\n";
    }
}

class Class02
{
    // attribute accessible to derived classes
    protected int $j = 0;

    // method
    public function doSomethingInClass02(): void
    {
        // modification of Class02::$j
        $this->j += 10;
        // display
        print "Class02->doSomethingInClass02... j=$this->j\n";
    }
}

// derived class
class Class03 extends Class02
{
    // inherits Class02::$j and includes Trait01::$i
    use Trait01;

    // method
    public function doSomethingInClass03(): void
    {
        // use of the method of Trait01
        $this->doSomethingInTrait01();
        // modification of Trait01::$i
        $this->i += 100;
        // modification of Class03::$j (==Class02::$j)
        $this->j += 1000;
        // display
        print "Class03->doSomethingInClass03... i=$this->i, j=$this->j\n";
    }
}

// test ----------------
(new Class02())->doSomethingInClass02();
(new Class03())->doSomethingInClass03();

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

1
2
3
4
> php trait-06.php
Class02->doSomethingInClass02... j=10
Trait01::doSomethingInTrait01... i=1
Class03->doSomethingInClass03... i=101, j=1000

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:

<?php

declare(strict_types=1);

class Class01
{
    // attribute
    private int $i = 0;

    // getter and setter
    public function getI(): int
    {
        return $this->i;
    }

    public function setI(int $i): void
    {
        $this->i = $i;
    }

    // method
    public function doSomethingInClass01(): void
    {
        // modification of Class01::$i
        $this->i++;
        // display
        print "Class01->doSomethingInClass01... i=$this->i\n";
    }
}

class Class02
{
    // attribute accessible to derived classes
    protected int $j = 0;

    // method specific to the class
    public function doSomethingInClass02(): void
    {
        // modification of Class02::$j
        $this->j += 10;
        // display
        print "Class02->doSomethingInClass02... j=$this->j\n";
    }
}

class Class03 extends Class02
{
    // a reference to an instance of Class01, injected by the constructor
    public function __construct(private readonly Class01 $class01)
    {
    }

    // method local to the class
    public function doSomethingInClass03(): void
    {
        // use of the method of Class01
        $this->class01->doSomethingInClass01();
        // modification of Class01::$i
        $i = $this->class01->getI();
        $i += 100;
        $this->class01->setI($i);
        // modification of Class03::$j
        $this->j += 1000;
        // display
        print "Class03->doSomethingInClass03... i=$i, j=$this->j\n";
    }
}

// test ----------------
$class02 = new Class02();
$class03 = new Class03(new Class01());
$class02->doSomethingInClass02();
$class03->doSomethingInClass03();

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

1
2
3
4
> php trait-07.php
Class02->doSomethingInClass02... j=10
Class01->doSomethingInClass01... i=1
Class03->doSomethingInClass03... i=101, j=1000

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 C that contains the code common to the derived classes. Class C implements interface I, but certain methods—which must be defined in the derived classes—are declared abstract in class C, 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]):

<?php

declare(strict_types=1);

interface Interface1
{
    public function doSomething(): void;

    public function doSomethingElse(): void;
}

abstract class AbstractClass implements Interface1
{
    // attributes accessible to derived classes
    protected int $attr1 = 11;
    protected int $attr2 = 12;

    // getters and setters
    public function getAttr1(): int
    {
        return $this->attr1;
    }

    public function getAttr2(): int
    {
        return $this->attr2;
    }

    public function setAttr1(int $attr1): static
    {
        $this->attr1 = $attr1;
        return $this;
    }

    public function setAttr2(int $attr2): static
    {
        $this->attr2 = $attr2;
        return $this;
    }

    // implemented method
    #[\Override]
    public function doSomething(): void
    {
        print "AbstractClass::doSomething [$this->attr1,$this->attr2]\n";
    }

    // method not implemented
    #[\Override]
    abstract public function doSomethingElse(): void;
}

// derived class 1
class Class1 extends AbstractClass
{
    // attribute
    private int $attr3 = 13;

    // getter and setter
    public function getAttr3(): int
    {
        return $this->attr3;
    }

    public function setAttr3(int $attr3): static
    {
        $this->attr3 = $attr3;
        return $this;
    }

    // implementation of doSomethingElse
    #[\Override]
    public function doSomethingElse(): void
    {
        print "Class1::doSomethingElse [$this->attr1,$this->attr2,$this->attr3]\n";
    }
}

// derived class 2
class Class2 extends AbstractClass
{
    // attribute
    private int $attr4 = 14;

    // getter and setter
    public function getAttr4(): int
    {
        return $this->attr4;
    }

    public function setAttr4(int $attr4): static
    {
        $this->attr4 = $attr4;
        return $this;
    }

    // implementation of doSomethingElse
    #[\Override]
    public function doSomethingElse(): void
    {
        print "Class2::doSomethingElse [$this->attr1,$this->attr2,$this->attr4]\n";
    }
}

// external function
function useInterfaceWith(Interface1 $interface): void
{
    $interface->doSomething();
    $interface->doSomethingElse();
}

// tests
useInterfaceWith(new Class1());
useInterfaceWith(new Class2());
// "fluent" setters can be chained
useInterfaceWith((new Class1())->setAttr1(21)->setAttr2(22)->setAttr3(23));

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

1
2
3
4
5
6
7
> php trait-08.php
AbstractClass::doSomething [11,12]
Class1::doSomethingElse [11,12,13]
AbstractClass::doSomething [11,12]
Class2::doSomethingElse [11,12,14]
AbstractClass::doSomething [21,22]
Class1::doSomethingElse [21,22,23]

Now we implement Solution 2 using the script [trait-09.php]. This involves replacing the abstract class with a trait:

<?php

declare(strict_types=1);

interface Interface1
{
    public function doSomething(): void;

    public function doSomethingElse(): void;
}

trait Trait1
{
    // attributes
    private int $attr1 = 11;
    private int $attr2 = 12;

    // getters and setters
    public function getAttr1(): int
    {
        return $this->attr1;
    }

    public function getAttr2(): int
    {
        return $this->attr2;
    }

    public function setAttr1(int $attr1): static
    {
        $this->attr1 = $attr1;
        return $this;
    }

    public function setAttr2(int $attr2): static
    {
        $this->attr2 = $attr2;
        return $this;
    }

    // implemented method
    #[\Override]
    public function doSomething(): void
    {
        print "Trait1::doSomething [$this->attr1,$this->attr2]\n";
    }
}

// class 1
class Class1 implements Interface1
{
    // use of the trait
    use Trait1;

    // attribute
    private int $attr3 = 13;

    // getter and setter
    public function getAttr3(): int
    {
        return $this->attr3;
    }

    public function setAttr3(int $attr3): static
    {
        $this->attr3 = $attr3;
        return $this;
    }

    // implementation of doSomethingElse
    #[\Override]
    public function doSomethingElse(): void
    {
        print "Class1::doSomethingElse [$this->attr1,$this->attr2,$this->attr3]\n";
    }
}

// class 2
class Class2 implements Interface1
{
    // use of the trait
    use Trait1;

    // attribute
    private int $attr4 = 14;

    // getter and setter
    public function getAttr4(): int
    {
        return $this->attr4;
    }

    public function setAttr4(int $attr4): static
    {
        $this->attr4 = $attr4;
        return $this;
    }

    // implementation of doSomethingElse
    #[\Override]
    public function doSomethingElse(): void
    {
        print "Class2::doSomethingElse [$this->attr1,$this->attr2,$this->attr4]\n";
    }
}

// external function using the interface
function useInterfaceWith(Interface1 $interface): void
{
    $interface->doSomething();
    $interface->doSomethingElse();
}

// tests
useInterfaceWith(new Class1());
useInterfaceWith(new Class2());
// "fluent" setters can be chained
useInterfaceWith((new Class1())->setAttr1(21)->setAttr2(22)->setAttr3(23));

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

1
2
3
4
5
6
7
> php trait-09.php
Trait1::doSomething [11,12]
Class1::doSomethingElse [11,12,13]
Trait1::doSomething [11,12]
Class2::doSomethingElse [11,12,14]
Trait1::doSomething [21,22]
Class1::doSomethingElse [21,22,23]

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:

<?php

declare(strict_types=1);

// a trait that requires something from the classes that use it
trait CounterTrait
{
    // trait constant (PHP 8.2)
    public const int STEP = 10;

    // static attribute: each using class has its own
    private static int $nbInstances = 0;

    // instance attribute
    private int $counter = 0;

    // static trait method
    public static function getNbInstances(): int
    {
        return self::$nbInstances;
    }

    // abstract method: the using class must define it (signature checked since PHP 8.0)
    abstract protected function getName(): string;

    // to be called by the constructor of the using class
    private function register(): void
    {
        self::$nbInstances++;
    }

    // concrete method that uses the abstract method and the constant
    public function increment(): static
    {
        $this->counter += self::STEP;
        print "{$this->getName()}: counter=$this->counter\n";
        return $this;
    }
}

class Car
{
    use CounterTrait;

    public function __construct(private string $registration)
    {
        $this->register();
    }

    // implementation of the abstract method of the trait
    #[\Override]
    protected function getName(): string
    {
        return "Car[$this->registration]";
    }
}

class Bike
{
    use CounterTrait;

    public function __construct()
    {
        $this->register();
    }

    #[\Override]
    protected function getName(): string
    {
        return "Bike";
    }
}

// tests
(new Car("AB-123-CD"))->increment()->increment();
new Car("EF-456-GH");
(new Bike())->increment();
// each class has its own static attribute
print "number of cars: " . Car::getNbInstances() . "\n";
print "number of bikes: " . Bike::getNbInstances() . "\n";
// the trait constant is accessible through the class, not through the trait
print "step: " . Car::STEP . "\n";
try {
    print CounterTrait::STEP . "\n";
} catch (\Error $ex) {
    print $ex::class . ": " . $ex->getMessage() . "\n";
}
// a class that uses the trait must implement the abstract method
// class Plane { use CounterTrait; }  // Fatal error: Class Plane contains 1 abstract method...

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

1
2
3
4
5
6
7
8
> php trait-10.php
Car[AB-123-CD]: counter=10
Car[AB-123-CD]: counter=20
Bike: counter=10
number of cars: 2
number of bikes: 1
step: 10
Error: Cannot access trait constant CounterTrait::STEP directly

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.