Skip to content

9. Les traits

Les traits sont des structures analogues à des classes. On ne peut cependant pas les instancier : ils sont destinés à être inclus dans des classes. L'inclusion d'un trait dans une classe a le même effet que si l'on avait copié le code du trait dans la classe. On met dans un trait du code susceptible d'être réutilisé dans plusieurs classes.

Les traits existent depuis PHP 5.4. PHP 8 leur a apporté quelques compléments que nous présenterons à la fin du chapitre : vérification de la signature des méthodes abstraites (PHP 8.0), constantes de trait (PHP 8.2), attribut [#[\Override]] utilisable dans un trait (PHP 8.3).

9.1. L'arborescence des scripts

Les scripts de ce chapitre sont dans le dossier [scripts-console/traits] :

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. Inclusion d'un trait dans une classe

Le script [trait-01.php] montre une utilisation basique d'un trait dans une classe :

<?php

declare(strict_types=1);

class Class04
{
    // constructeur avec promotion de l'attribut [name]
    public function __construct(private string $name)
    {
    }

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

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

trait Trait04
{
    // attribut
    private string $name;

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

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

class Class05
{
    // inclusion du trait
    use Trait04;

    // constructeur
    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";
// affichage des deux objets
print_r($class04);
print_r($class05);

Commentaires du code

  • lignes 5-22 : définition de la classe [Class04] avec un attribut [$name] (promu par le constructeur), son getter, son setter ;
  • lignes 24-39 : on reprend le code de [Class04] sans son constructeur et on le transfère dans le trait [Trait04]. L'attribut est cette fois déclaré explicitement, avec son type. On ne reprend pas le constructeur puisqu'un trait n'est pas instanciable ;
  • ligne 24 : c'est le mot clé [trait] qui fait de [Trait04] un trait plutôt qu'une classe ;
  • lignes 41-51 : on définit une classe [Class05] qui inclut le code du trait [Trait04] et lui ajoute un constructeur, pour rendre la classe instanciable ;
  • ligne 44 : c'est le mot clé [use], placé à l'intérieur de la classe, qui permet l'inclusion d'un trait dans une classe. Il ne faut pas le confondre avec l'instruction [use] placée en dehors de toute classe, qui importe un nom d'un espace de noms ;
  • lignes 54-60 : des tests montrent que les classes [Class04] et [Class05] fonctionnent de la même façon .

Résultats

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

Les résultats des lignes 4-11 montrent que les objets [Class04] et [Class05] ont le même contenu : l'attribut privé [$name] du trait est devenu un attribut privé de la classe [Class05].

Conclusion : l'utilisation de l'instruction [use Trait] dans une classe est équivalente à inclure le code de [Trait] dans la classe.

9.3. Utiliser un même trait dans différentes classes

Un premier intérêt du trait semble être la réutilisation d'un même code (attributs + méthodes) dans différentes classes. Nous allons voir cependant qu'on peut arriver au même objectif en utilisant de simples classes.

Le partage d'un trait entre classes est illustré par le script [trait-02.php] suivant :

<?php

declare(strict_types=1);

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

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

class Class02
{
    // inclusion de Trait01
    use Trait01 {
        // la méthode [Trait01::doSomething] est accessible
        // dans la classe sous le nom [doSomethingInTrait]
        Trait01::doSomething as doSomethingInTrait;
    }

    // méthode propre à la classe
    public function doSomething(): void
    {
        // attribut id
        $this->id += 10;
        // utilisation de la méthode de Trait01
        $this->doSomethingInTrait();
        // affichage local
        print "Class02->doSomething\n";
    }
}

class Class03
{
    // inclusion de Trait01
    use Trait01;

    // méthode locale à la classe
    public function doSomethingElse(): void
    {
        // attribut id
        $this->id += 10;
        // utilisation de la méthode de Trait01
        $this->doSomething();
        // affichage local
        print "Class03->doSomethingElse\n";
    }
}

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

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

Commentaires

  • lignes 5-15 : un trait définissant un attribut [$id] et une méthode [doSomething] ;
  • le trait [Trait01] est inclus dans deux classes [Class02] (lignes 17-36) et [Class03] (lignes 38-53) ;
  • lignes 20-24 : inclusion de [Trait01] dans [Class02]. L'inclusion est suivie d'un bloc [{...}] qui permet d'ajuster les méthodes incluses ;
  • ligne 23 : cette ligne résout un conflit : [Trait01] et [Class02] ont tous les deux une méthode appelée [doSomething]. Dans ce cas, c'est la méthode de la classe qui l'emporte : la méthode du trait est masquée. L'opérateur [as] rend la méthode [Trait01::doSomething] accessible dans la classe sous le nom [doSomethingInTrait]. Ainsi, à l'intérieur de [Class02], on utilisera les notations :
    • [doSomethingInTrait] pour appeler la méthode [Trait01::doSomething] ;
    • [doSomething] pour appeler la méthode [Class02::doSomething] ;
  • lignes 30-32 : la classe [Class02] utilise l'attribut et la méthode de [Trait01] comme s'ils lui étaient propres ;
  • lignes 38-53 : la classe [Class03] est analogue à la classe [Class02]. L'inclusion de [Trait01] est ici plus simple parce qu'il n'y a pas de collision entre les méthodes de [Trait01] et de [Class03] .

Résultats

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

On notera bien qu'il n'y a pas partage du trait [Trait01] entre les classes [Class02] et [Class03]. L'attribut [$id] de [Trait01] devient, par inclusion de [Trait01] dans les classes [Class02] et [Class03], deux attributs différents [Class02::$id] et [Class03::$id]. C'est ce que montrent les lignes 3 et 5 des résultats. Si l'attribut [$id] avait été partagé entre les classes [Class02] et [Class03], on aurait eu 20 à la ligne 5 au lieu de 10.

Le script [trait-03.php] montre qu'on peut arriver au même résultat en utilisant une classe au lieu du trait :

<?php

declare(strict_types=1);

// classe qui remplace le trait
class Class01
{
    // attribut
    private int $id = 0;

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

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

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

class Class02
{
    // une référence sur une instance de Class01, injectée par le constructeur
    public function __construct(private readonly Class01 $class01)
    {
    }

    // méthode propre à la classe
    public function doSomething(): void
    {
        // modification de l'attribut de Class01
        $id = $this->class01->getId();
        $id += 10;
        $this->class01->setId($id);
        // utilisation de la méthode de Class01
        $this->class01->doSomething();
        // affichage local
        print "Class02->doSomething\n";
    }
}

class Class03
{
    // une référence sur une instance de Class01, injectée par le constructeur
    public function __construct(private readonly Class01 $class01)
    {
    }

    // méthode locale à la classe
    public function doSomethingElse(): void
    {
        // modification de l'attribut de Class01
        $id = $this->class01->getId();
        $id += 10;
        $this->class01->setId($id);
        // utilisation de la méthode de Class01
        $this->class01->doSomething();
        // affichage local
        print "Class03->doSomethingElse\n";
    }
}

// test01 ----------------
function test01(): void
{
    // deux objets qui vont accéder à deux instances différentes de [Class01]
    $class02 = new Class02(new Class01());
    $class03 = new Class03(new Class01());
    // vérification
    $class02->doSomething();
    $class03->doSomethingElse();
}

// test02 ----------------
function test02(): void
{
    // instance partagée de [Class01]
    $class01 = new Class01();
    // deux objets qui vont accéder à la même instance de [Class01]
    $class02 = new Class02($class01);
    $class03 = new Class03($class01);
    // vérification
    $class02->doSomething();
    $class03->doSomethingElse();
}

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

Commentaires

  • lignes 6-28 : la classe [Class01] remplace le trait [Trait01]. Le code de [Trait01] était incorporé au code des classes [Class02] et [Class03]. Ici, ce ne sera pas le cas : c'est une référence à un objet [Class01] qui sera injectée dans les classes [Class02] et [Class03]. Les attributs de la classe [Class01] seront donc extérieurs aux objets [Class02] et [Class03]. Comme l'attribut [$id] est privé, il faut prévoir un getter et un setter. C'est la 1re différence avec le trait : il faut créer le code d'accès aux attributs privés de la classe. On aurait pu rendre l'attribut [$id] public mais ce n'est pas conseillé : passer par un setter pour fixer la valeur d'un attribut permet d'en vérifier la validité ;
  • ligne 33 : la référence à l'objet [Class01] est injectée dans [Class02] par son constructeur, grâce à la promotion des paramètres. Le cours PHP 7 utilisait un setter [setClass01]. L'injection par le constructeur est préférable : l'objet [Class02] est complet dès sa construction et, l'attribut étant [readonly], la référence ne peut plus être changée ;
  • lignes 41-45 : pour tout usage du code de [Class01], il nous faut passer par l'attribut [$this->class01] ;
  • lignes 51-70 : la classe [Class03] est un clone de la classe [Class02] si ce n'est que sa méthode a un nom différent ;
  • lignes 73-81 : le 1er test. Celui-ci consiste à injecter dans les objets [Class02] et [Class03] deux instances différentes de la classe [Class01] ;
  • lignes 84-94 : le 2e test injecte dans les objets [Class02] et [Class03] la même instance de la classe [Class01] .

Résultats

> 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

Commentaires des résultats

  • lignes 3-6 : on obtient les mêmes résultats qu'avec le trait [Trait01]. On en conclura que l'usage du trait n'est ici pas indispensable mais qu'il amène une réduction du code due au fait qu'il n'y a pas besoin de méthodes pour accéder aux attributs du trait : ceux-ci font partie intégrante de la classe dans laquelle le trait a été incorporé ;
  • lignes 8-11 : du fait qu'on a injecté la même référence de [Class01] dans les objets [Class02] et [Class03], l'attribut [Class01::$id] a été partagé entre les deux objets. C'est pour cela que la ligne 10 des résultats affiche 20 au lieu de 10 avec le trait. On en conclura que si des attributs doivent être partagés entre des objets, le trait n'est pas utilisable et il faut alors utiliser une classe .

9.4. Regrouper des méthodes dans un trait

Dans l'exemple précédent, le trait comportait des attributs et des méthodes. Nous considérons ici le cas où il ne contient que des méthodes. Le trait ressemble alors à une factorisation de méthodes qu'on peut utiliser dans différentes classes. Comme le trait n'a ici pas d'attribut, on va considérer le cas où les méthodes qu'il rassemble travaillent uniquement sur des paramètres qu'on leur passe. Ce n'est pas obligatoire : un trait peut travailler sur un attribut [$this->attribut1] sans avoir cet attribut. C'est alors aux classes qui utilisent ce trait de fournir l'attribut [$this->attribut1].

Dans le cas où le trait n'a que des méthodes qui travaillent uniquement sur des paramètres qu'on leur passe, nous montrerons que le trait peut être remplacé par une classe ayant les mêmes méthodes que le trait, déclarées statiques.

L'utilisation du trait est illustrée par le script [trait-04.php] qui reprend le trait de l'exemple précédent en lui enlevant tout attribut :

<?php

declare(strict_types=1);

trait Trait01
{
    // méthode à partager
    public function doSomething(): void
    {
        print "Trait01::doSomething...\n";
    }
}

class Class02
{
    // inclusion de Trait01
    use Trait01 {
        // la méthode [Trait01::doSomething] est accessible
        // dans la classe sous le nom [doSomethingInTrait]
        Trait01::doSomething as doSomethingInTrait;
    }

    public function doSomething(): void
    {
        // appel de la méthode de Trait01
        $this->doSomethingInTrait();
        // affichage local
        print "Class02->doSomething\n";
    }
}

class Class03
{
    // inclusion de Trait01
    use Trait01;

    // méthode locale à la classe
    public function doSomethingElse(): void
    {
        // appel de la méthode de Trait01
        $this->doSomething();
        // affichage local
        print "Class03->doSomethingElse\n";
    }
}

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

Commentaires

  • lignes 5-12 : le trait [Trait01] n'a plus d'attribut ;
  • lignes 17-21 : inclusion de [Trait01] dans [Class02]. La méthode de [Trait01] est utilisée ligne 26 ;
  • ligne 35 : inclusion de [Trait01] dans [Class03]. La méthode de [Trait01] est utilisée ligne 41 .

Résultats

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

Dans ce cas d'usage, le trait [Trait01] peut aisément être remplacé par une classe. C'est ce que montre le script [trait-05.php] suivant :

<?php

declare(strict_types=1);

// classe ni instanciable, ni dérivable, qui ne contient que des méthodes statiques
final class Class01
{
    // constructeur privé : pas d'instanciation possible hors de la classe
    private function __construct()
    {
    }

    // méthode statique à partager
    public static function doSomething(): void
    {
        print "Class01::doSomething...\n";
    }
}

class Class02
{
    public function doSomething(): void
    {
        // appel de la méthode statique de Class01
        Class01::doSomething();
        // affichage local
        print "Class02->doSomething\n";
    }
}

class Class03
{
    // méthode locale à la classe
    public function doSomethingElse(): void
    {
        // appel de la méthode statique de Class01
        Class01::doSomething();
        // affichage local
        print "Class03->doSomethingElse\n";
    }
}

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

Commentaires

  • lignes 6-18 : le trait [Trait01] est remplacé par une classe [Class01] dont toutes les méthodes sont statiques. Il est inutile de l'instancier ou de la dériver. Le cours PHP 7 la déclarait abstraite pour empêcher son instanciation, en regrettant que PHP refuse le préfixe [final abstract]. C'est toujours le cas en PHP 8 : on déclare plutôt la classe [final] (non dérivable) avec un constructeur privé (non instanciable). C'est ce que nous avons fait pour la classe [Utilitaires] de la version 3 de l'exercice d'application ;
  • ligne 25 : au lieu d'écrire [$this->doSomethingInTrait()], on écrit maintenant [Class01::doSomething()], c'est-à-dire qu'on appelle la méthode statique [doSomething] de la classe [Class01] ;
  • ligne 37 : on répète la même démarche dans [Class03] .

Résultats

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

On a bien le même résultat qu'avec le trait [Trait01], ce qui montre que l'usage de celui-ci peut être évité. On a écrit que les méthodes d'un trait peuvent travailler sur un attribut [$this->attribut1] sans que le trait ait cet attribut, à charge pour les classes qui utilisent ce trait de le fournir. C'est un cas exotique et fragile : autant « remonter » l'attribut [$this->attribut1] dans le trait lui-même. Ainsi il fera forcément partie des attributs de la classe utilisant le trait. Nous verrons à la fin du chapitre que PHP 8 offre une solution propre pour exiger quelque chose des classes qui utilisent un trait : les méthodes abstraites.

9.5. Héritage multiple avec un trait

Il est fréquent de lire dans la littérature PHP que le trait permet l'héritage multiple : la possibilité pour une classe d'hériter de plusieurs classes. Le langage C++ possède cette possibilité mais pas les langages Java ou C# qui ne connaissent que l'héritage simple. Nous allons montrer que si l'usage d'un trait dans une classe dérivée permet effectivement d'implémenter quelque chose qui ressemble à l'héritage multiple, ce cas d'usage peut là encore s'implémenter avec de simples classes.

Le script [trait-06.php] met en œuvre une classe dérivée et un trait :

<?php

declare(strict_types=1);

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

    // méthode à partager
    public function doSomethingInTrait01(): void
    {
        // modification de Trait01::$i
        $this->i++;
        // affichage
        print "Trait01::doSomethingInTrait01... i=$this->i\n";
    }
}

class Class02
{
    // attribut accessible aux classes dérivées
    protected int $j = 0;

    // méthode
    public function doSomethingInClass02(): void
    {
        // modification de Class02::$j
        $this->j += 10;
        // affichage
        print "Class02->doSomethingInClass02... j=$this->j\n";
    }
}

// classe dérivée
class Class03 extends Class02
{
    // hérite de Class02::$j et inclut Trait01::$i
    use Trait01;

    // méthode
    public function doSomethingInClass03(): void
    {
        // utilisation de la méthode de Trait01
        $this->doSomethingInTrait01();
        // modification de Trait01::$i
        $this->i += 100;
        // modification de Class03::$j (==Class02::$j)
        $this->j += 1000;
        // affichage
        print "Class03->doSomethingInClass03... i=$this->i, j=$this->j\n";
    }
}

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

Commentaires

  • lignes 5-18 : un trait [Trait01] avec un attribut et une méthode manipulant celui-ci ;
  • lignes 20-33 : une classe [Class02] qui n'a rien à voir avec le trait [Trait01]. Elle ne l'utilise pas ;
  • ligne 23 : l'unique attribut de [Class02] a la visibilité [protected] pour être accessible dans les classes dérivées ;
  • ligne 36 : la classe [Class03] étend la classe [Class02]. De plus, elle incorpore le trait [Trait01] (ligne 39). Finalement, elle hérite des attributs et méthodes de [Class02] et incorpore les attributs et méthodes de [Trait01]. On a donc bien quelque chose d'analogue à l'héritage multiple .

Résultats

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

De la même façon que dans un exemple précédent, nous allons montrer que :

  • le trait peut être remplacé par une classe ;
  • au lieu d'incorporer le trait dans la classe dérivée, on lui injecte la référence d'une instance de cette classe .

Le script [trait-07.php] est le suivant :

<?php

declare(strict_types=1);

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

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

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

    // méthode
    public function doSomethingInClass01(): void
    {
        // modification de Class01::$i
        $this->i++;
        // affichage
        print "Class01->doSomethingInClass01... i=$this->i\n";
    }
}

class Class02
{
    // attribut accessible aux classes dérivées
    protected int $j = 0;

    // méthode propre à la classe
    public function doSomethingInClass02(): void
    {
        // modification de Class02::$j
        $this->j += 10;
        // affichage
        print "Class02->doSomethingInClass02... j=$this->j\n";
    }
}

class Class03 extends Class02
{
    // une référence sur une instance de Class01, injectée par le constructeur
    public function __construct(private readonly Class01 $class01)
    {
    }

    // méthode locale à la classe
    public function doSomethingInClass03(): void
    {
        // utilisation de la méthode de Class01
        $this->class01->doSomethingInClass01();
        // modification de Class01::$i
        $i = $this->class01->getI();
        $i += 100;
        $this->class01->setI($i);
        // modification de Class03::$j
        $this->j += 1000;
        // affichage
        print "Class03->doSomethingInClass03... i=$i, j=$this->j\n";
    }
}

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

Commentaires

  • lignes 5-29 : la classe [Class01] remplace le trait [Trait01]. Comme un objet [Class01] va être utilisé via une référence, on a prévu un getter et un setter pour l'attribut [$i] ;
  • lignes 31-44 : la classe [Class02] ne change pas ;
  • ligne 46 : la classe [Class03] étend la classe [Class02] ;
  • ligne 49 : une référence à un objet [Class01] est injectée dans [Class03] par son constructeur ;
  • lignes 54-66 : la méthode [doSomethingInClass03] fait la même chose que précédemment avec cependant un code plus complexe .

Résultats

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

De cet exemple, on peut conclure que là encore le trait n'est pas indispensable, mais il faut reconnaître qu'il permet l'écriture d'un code plus court dans la classe dérivée.

9.6. Utiliser un trait à la place d'une classe abstraite

On rencontre souvent le cas d'utilisation suivant : on crée une interface I assez générale qui peut donner naissance à plusieurs implémentations. Celles-ci partagent un code commun mais diffèrent par d'autres méthodes. On peut implémenter ce cas d'utilisation de deux façons :

  • on crée une classe abstraite C qui regroupe le code commun aux classes dérivées. La classe C implémente l'interface I mais certaines méthodes, qui doivent être définies dans les classes dérivées, sont déclarées abstraites dans la classe C, qui est donc elle-même abstraite. On crée ensuite des classes C1 et C2 dérivées de C qui implémentent chacune à leur manière les méthodes abstraites de leur classe parent C ;
  • on crée un trait T quasi identique à la classe abstraite C de la solution précédente. Ce trait n'implémente pas l'interface I car syntaxiquement il ne le peut pas. On crée ensuite des classes C1 et C2 implémentant l'interface I et utilisant le trait T. Il ne reste plus à ces classes qu'à implémenter les méthodes de l'interface I non implémentées par le trait T qu'elles importent .

Voici un exemple qui montre la grande proximité de ces deux solutions. L'application 1 implémente la 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
{
    // attributs accessibles aux classes dérivées
    protected int $attr1 = 11;
    protected int $attr2 = 12;

    // getters et 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;
    }

    // méthode implémentée
    #[\Override]
    public function doSomething(): void
    {
        print "AbstractClass::doSomething [$this->attr1,$this->attr2]\n";
    }

    // méthode non implémentée
    #[\Override]
    abstract public function doSomethingElse(): void;
}

// classe dérivée 1
class Class1 extends AbstractClass
{
    // attribut
    private int $attr3 = 13;

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

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

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

// classe dérivée 2
class Class2 extends AbstractClass
{
    // attribut
    private int $attr4 = 14;

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

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

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

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

// tests
useInterfaceWith(new Class1());
useInterfaceWith(new Class2());
// les setters « fluents » peuvent être enchaînés
useInterfaceWith((new Class1())->setAttr1(21)->setAttr2(22)->setAttr3(23));

Commentaires

  • lignes 5-10 : l'interface [Interface1] a deux méthodes ;
  • ligne 12 : la classe abstraite [AbstractClass] implémente l'interface [Interface1]. Elle a deux attributs avec leurs getters et setters, implémente la méthode [doSomething] de l'interface [Interface1] (ligne 43) mais ne sait pas implémenter la méthode [doSomethingElse]. Celle-ci est donc déclarée abstraite (ligne 50). La classe abstraite [AbstractClass] ne peut être instanciée et pour servir à quelque chose elle doit obligatoirement être dérivée ;
  • lignes 29-39 : les setters sont « fluents » : au lieu de ne rien rendre, ils rendent l'objet [$this]. Cela permet d'enchaîner les appels (ligne 116). Le type de retour [static] (PHP 8.0) désigne la classe de l'objet sur lequel la méthode a été appelée : [(new Class1())->setAttr1(21)] est de type [Class1] et non [AbstractClass], ce qui permet d'appeler ensuite [setAttr3], méthode propre à [Class1] ;
  • ligne 54 : la classe [Class1] étend la classe abstraite [AbstractClass] et donc implémente l'interface [Interface1]. Elle donne un corps à la méthode [doSomethingElse] que sa classe parent n'avait pas définie. Elle ajoute également un attribut à ceux de sa classe parent ;
  • ligne 80 : la classe [Class2] fait de même avec un autre attribut ;
  • lignes 106-110 : la fonction [useInterfaceWith] reçoit en paramètre un type [Interface1] et appelle les deux méthodes de cette interface ;
  • lignes 113-116 : on appelle la fonction [useInterfaceWith] avec un type [Class1] puis avec un type [Class2]. C'est correct puisque ces deux types implémentent l'interface [Interface1] .

Résultats

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]

Maintenant nous implémentons la solution 2 avec le script [trait-09.php]. Cela consiste à remplacer la classe abstraite par un trait :

<?php

declare(strict_types=1);

interface Interface1
{
    public function doSomething(): void;

    public function doSomethingElse(): void;
}

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

    // getters et 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;
    }

    // méthode implémentée
    #[\Override]
    public function doSomething(): void
    {
        print "Trait1::doSomething [$this->attr1,$this->attr2]\n";
    }
}

// classe 1
class Class1 implements Interface1
{
    // utilisation du trait
    use Trait1;

    // attribut
    private int $attr3 = 13;

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

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

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

// classe 2
class Class2 implements Interface1
{
    // utilisation du trait
    use Trait1;

    // attribut
    private int $attr4 = 14;

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

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

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

// fonction externe utilisant l'interface
function useInterfaceWith(Interface1 $interface): void
{
    $interface->doSomething();
    $interface->doSomethingElse();
}

// tests
useInterfaceWith(new Class1());
useInterfaceWith(new Class2());
// les setters « fluents » peuvent être enchaînés
useInterfaceWith((new Class1())->setAttr1(21)->setAttr2(22)->setAttr3(23));

Commentaires

  • lignes 5-10 : l'interface [Interface1] n'a pas changé ;
  • lignes 12-47 : le trait [Trait1] remplace la classe abstraite [AbstractClass] de la solution 1. Le code est le même aux détails près suivants :
    • ligne 12 : le trait [Trait1] n'implémente pas l'interface [Interface1]. C'est syntaxiquement impossible ;
    • lignes 15-16 : la visibilité [protected] des attributs de la classe abstraite [AbstractClass] devient ici [private]. Dans les deux cas, le but est de donner aux classes concrètes un accès direct aux attributs sans passer par les getters et setters. Avec un trait, [private] suffit puisque les attributs du trait deviennent ceux de la classe ;
    • le trait [Trait1] ne déclare pas la méthode abstraite [doSomethingElse] ;
    • ligne 42 : depuis PHP 8.3, l'attribut [#[\Override]] peut être placé sur une méthode de trait. Il est vérifié dans chaque classe qui utilise le trait : ici, la méthode [doSomething] doit correspondre à une méthode d'une interface ou d'une classe parent de [Class1] et de [Class2]. C'est bien le cas : c'est la méthode [doSomething] de [Interface1] ;
  • ligne 50 : la classe [Class1] de la solution 2 est identique à la classe [Class1] de la solution 1 aux détails près suivants :
    • elle implémente l'interface [Interface1] alors que dans la solution 1, elle étendait la classe abstraite [AbstractClass] ;
    • elle utilise le trait [Trait1] (ligne 53) pour implémenter une partie de l'interface ;
  • ligne 79 : on peut faire les mêmes commentaires que pour [Class1] ;
  • le reste du code ne change pas .

Résultats

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]

On obtient bien les mêmes résultats.

9.7. Les traits en PHP 8

PHP 8 a rendu les traits plus sûrs et plus complets. Le script [trait-10.php] illustre ces nouveautés :

<?php

declare(strict_types=1);

// un trait qui exige quelque chose des classes qui l'utilisent
trait TraitCompteur
{
    // constante de trait (PHP 8.2)
    public const int PAS = 10;

    // attribut statique : chaque classe utilisatrice a le sien
    private static int $nbInstances = 0;

    // attribut d'instance
    private int $compteur = 0;

    // méthode statique de trait
    public static function getNbInstances(): int
    {
        return self::$nbInstances;
    }

    // méthode abstraite : la classe utilisatrice doit la définir (signature vérifiée depuis PHP 8.0)
    abstract protected function getNom(): string;

    // à appeler par le constructeur de la classe utilisatrice
    private function enregistrer(): void
    {
        self::$nbInstances++;
    }

    // méthode concrète qui utilise la méthode abstraite et la constante
    public function incrémenter(): static
    {
        $this->compteur += self::PAS;
        print "{$this->getNom()} : compteur=$this->compteur\n";
        return $this;
    }
}

class Voiture
{
    use TraitCompteur;

    public function __construct(private string $immatriculation)
    {
        $this->enregistrer();
    }

    // implémentation de la méthode abstraite du trait
    #[\Override]
    protected function getNom(): string
    {
        return "Voiture[$this->immatriculation]";
    }
}

class Velo
{
    use TraitCompteur;

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

    #[\Override]
    protected function getNom(): string
    {
        return "Vélo";
    }
}

// tests
(new Voiture("AB-123-CD"))->incrémenter()->incrémenter();
new Voiture("EF-456-GH");
(new Velo())->incrémenter();
// chaque classe a son propre attribut statique
print "nombre de voitures : " . Voiture::getNbInstances() . "\n";
print "nombre de vélos : " . Velo::getNbInstances() . "\n";
// la constante du trait est accessible via la classe, pas via le trait
print "pas : " . Voiture::PAS . "\n";
try {
    print TraitCompteur::PAS . "\n";
} catch (\Error $ex) {
    print $ex::class . " : " . $ex->getMessage() . "\n";
}
// une classe qui utilise le trait doit implémenter la méthode abstraite
// class Avion { use TraitCompteur; }  // Fatal error : Class Avion contains 1 abstract method...

Commentaires

  • ligne 9 : depuis PHP 8.2, un trait peut déclarer des constantes. Elles deviennent des constantes de la classe qui utilise le trait ;
  • ligne 12 : un trait peut déclarer des attributs statiques. Comme les attributs d'instance, ils sont recopiés dans chaque classe qui utilise le trait : chaque classe a son propre compteur d'instances ;
  • lignes 18-21 : un trait peut déclarer des méthodes statiques. Dans le code du trait, [self] désigne la classe qui utilise le trait ;
  • ligne 24 : un trait peut déclarer des méthodes abstraites. Toute classe qui utilise le trait doit les implémenter, sinon elle doit elle-même être déclarée abstraite (voir la classe [Avion] en commentaire à la fin du script). Depuis PHP 8.0, la signature de l'implémentation (types des paramètres et du résultat) est vérifiée. C'est la façon propre d'exiger quelque chose des classes qui utilisent le trait, plutôt que de supposer qu'elles ont tel attribut ou telle méthode ;
  • lignes 33-38 : une méthode concrète du trait qui utilise la méthode abstraite [getNom] et la constante [PAS]. Elle rend [$this] (type [static]) pour pouvoir être enchaînée ;
  • lignes 41-56 : la classe [Voiture] utilise le trait, appelle la méthode privée [enregistrer] du trait dans son constructeur et implémente la méthode abstraite [getNom]. On remarquera l'attribut [#[\Override]] sur cette implémentation : il est accepté parce que la méthode implémente une méthode abstraite du trait ;
  • lignes 58-72 : la classe [Velo] fait de même ;
  • lignes 79-80 : chaque classe a son propre attribut statique [$nbInstances] ;
  • ligne 82 : la constante du trait est accessible via la classe qui l'utilise ;
  • ligne 84 : mais pas directement via le trait : on obtient une erreur .

Résultats

1
2
3
4
5
6
7
8
> php trait-10.php
Voiture[AB-123-CD] : compteur=10
Voiture[AB-123-CD] : compteur=20
Vélo : compteur=10
nombre de voitures : 2
nombre de vélos : 1
pas : 10
Error : Cannot access trait constant TraitCompteur::PAS directly

9.8. Conclusion

Des exemples précédents, il ressort que les cas d'utilisation où l'usage du trait amène un avantage net ne sont pas évidents. Sur nos exemples, on peut toujours s'en passer en le remplaçant par une classe. Son utilisation est cependant pratique pour factoriser du code entre différentes classes, comme si ce code appartenait à une classe parent, sans consommer l'unique héritage dont dispose une classe PHP. C'est ce que nous ferons dans la version 4 de l'exercice d'application.