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] :
9.2. Inclusion d'un trait dans une classe
Le script [trait-01.php] montre une utilisation basique d'un trait dans une classe :
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
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 :
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
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 :
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
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 :
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
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 :
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
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 :
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
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 :
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
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] :
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 | |
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
Maintenant nous implémentons la solution 2 avec le script [trait-09.php]. Cela consiste à remplacer la classe abstraite par un 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 | |
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
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 :
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
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.