Un trait en PHP est un regroupement de une ou plusieurs méthodes au sein d'un même conteneur qui peut être utilisé par plusieurs classes.
Attention, un trait n'a rien à voir avec une interface, qui ne doit obligatoirement définir que la signature de une ou plusieurs méthodes, leurs définitions étant à la charge des classes implémentant l'interface.
En effet, un trait doit contenir la définition des méthodes, et non uniquement leurs signatures.
Il peut cependant contenir des méthodes abstraites, et dans ce cas, les classes qui l'utilisent devront définir ces dernières.
Par ailleurs, un trait est indépendant de la structure interne des classes qui seront amenées à l'utiliser.
En conséquence, il n'est pas possible d'utiliser directement des propriétés de classe dans un trait, d'ou l'utilisation de méthodes abstraites pour définir l'interface qui lui permettra de manipuler les données de l'objet concerné.
Le plus simple pour comprendre le concept est de se baser sur un exemple concret.
Supposons que vous souhaitez représenter au format JSON des instances de deux classes différentes.
Le code pourrait être le suivant, avec les dernières versions de PHP :
<?php
interface jsonizable
{
public function toJSON();
}
class A implements jsonizable
{
protected $title = '';
protected $paragraphes = '';
...
public function toJSON()
{
return json_encode(array('title' => $this->title, 'paragraphes' => $this->paragraphes));
}
...
}
class B implements jsonizable
{
protected $firstName = '';
protected $lastName = '';
...
public function toJSON()
{
return json_encode(array('firstName' => $this->firstName, 'lastName' => $this->lastName));
}
...
}
$a = new A(uniqid(), array(uniqid(), uniqid()));
$aJSON = $a->toJSON();
$b = new B(uniqid(), uniqid());
$bJSON = $b->toJSON();
?>
La prochaine version de PHP permettra l'utilisation de l'interface JsonSerializable pour obtenir le même résultat.
Le code sera alors le suivant :
<?php
class A implements JsonSerializable
{
protected $title = '';
protected $paragraphes = '';
...
public function jsonSerialize()
{
return array('title' => $this->title, 'paragraphes' => $this->paragraphes);
}
...
}
class B implements JsonSerializable
{
protected $firstName = '';
protected $lastName = '';
...
public function jsonSerialize()
{
return array('firstName' => $this->firstName, 'lastName' => $this->lastName);
}
...
}
$a = new A(uniqid(), array(uniqid(), uniqid()));
$aJSON = json_encode($a);
$b = new A(uniqid(), uniqid());
$bJSON = json_encode($b);
?>
Et la même problématique aurait pu également être résolue à l'aide d'un trait, de la manière suivante :
<?php
trait jsonSerializable
{
function toJSON()
{
return json_encode($this->getJSONProperties());
}
public abstract function getJSONProperties();
}
class A
{
use jsonSerializable;
protected $title = '';
protected $paragraphes = '';
...
public function getJSONProperties()
{
return array('title' => $this->title, 'paragraphes' => $this->paragraphes);
}
...
}
class B
{
use jsonSerializable;
protected $firstName = '';
protected $lastName = '';
...
public function getJSONProperties()
{
return array('firstName' => $this->firstName, 'lastName' => $this->lastName);
}
...
}
$a = new A(uniqid(), array(uniqid(), uniqid()));
$aJSON = $a->toJSON();
$b = new A(uniqid(), uniqid());
$bJSON = $b->toJSON();
?>
Je précise cependant que cette solution ne présente aucun intérêt, du fait de l'existence de l'interface JsonSerializable, et ne me permet dans le cas présent que d'illustrer le fonctionnement des traits.
Petite précision, il est tout à fait possible d'aggréger des traits au sein d'un trait et en conséquence, les possibilités qu'ils offrent sont très intéressantes.
Et moi, j'ai réussi à expliquer deux futures fonctionnalités de PHP dans un même billet, que metagoto en soit remercié !
Mise à jour : metagoto a écrit un billet complémentaire sur le sujet, à lire absolument si vous voulez savoir comment tout fonctionne sous le capot.
10 réactions
1 De desfrenes - 17/05/2010, 14:27
Un peu de l'héritage multiple, moins quelques inconvénients que l'on peut avoir en Python. Je trouve curieux de réutiliser le mot clé "use" mais bon...
2 De mageekguy - 17/05/2010, 14:44
@desfrenes : Vu la façon dont évolue PHP, ce que contient ce billet ne sera peut être plus d'actualité lors de la sortie de la prochaine version.
La syntaxe peut très bien être revue, même si je n'y crois pas, d'autant que
use
me convient très bien.Tout cela est plus destiné à faire comprendre le concept qu'un véritable tutoriel sur les traits.
3 De metagoto - 17/05/2010, 19:10
Je viens de relire rapidos le rfc des traits. Effectivement, ça n'est pas d'une grande utilité pour ce cas de jsonable puisque ces derniers n'implémentent pas d'interface et surtout il n'y a pas de bind sur $this automatique (autres que les abstract methods). Les grafts sembleraient mieux adaptés pour ça.
Mais que se passe-t-il si une classe déclare qu'elle implémente une interface et cette implémentation est effectuée par un trait ?
En tout cas, merci pour l'article.
4 De metagoto - 18/05/2010, 00:41
Ce problème de bind automatique de $this m'a tracassé, c'est à dire "il n'est pas possible d'utiliser directement des propriétés de classe dans un trait", comme l'affirme @fch dans son billet. Cela limiterait très fortement l'intérêt des traits, tout comme l'impossibilité d'utiliser $this->func() dans un trait si func() n'est pas définie/déclarée dans ce trait (possiblement abstract).
Je n'ai pas compilé le trunk mais j'ai parcouru les sources online (notamment zend_compile.c). D'après ce que je vois, en fait, il n'y a pas de restriction d'usage de $this dans les traits. Les méthodes des traits sont injectées (moyennant conditions d'aliasing et d'overloading) dans la "table des méthodes" de la classe. A moins que je me fourvoie, et bien ça me rassure! Et du coup, j'ai l'impression que rien n'empêche de "déléguer" à des traits l'implémentation d'interfaces que la classe concrète est censée implémenter.
A vérifier quand même... @fch, tu utilises un php trunk ? Tu peux confirmer/infirmer ? Je vais tacher de tester ça dans les jours à venir.
5 De mageekguy - 18/05/2010, 08:07
@metagoto : Je me suis basé sur la phrase de la RFC des traits pour affirmer cela.
Et je n'ai pas dis, et la RFC non plus, que l'utilisation de
$this
était prohibée dans un trait, j'ai dis que l'utilisation des propriétés l'était.Donc, en théorie, il n'est pas possible d'utiliser $this->var dans un trait, et il faut passer par un mécanisme de getter/setter défini par des méthodes abstraites dans le trait, qui définissent l'interface de communication entre lui-même et les classes qui l'utilise.
Cela ne me choque pas étant donné qu'un trait doit être indépendant de l'implémentation des classes qui l'utilise pour être suffisament générique et donc intéressant pour le développeur.
Maintenant, rien ne dit que PHP fait un quelconque contrôle à ce niveau, ce serait d'ailleurs bien dans le style du langage de ne rien faire.
Je me dis également que si l'utilisation des propriétés est possible, il faut gérer le niveau de visibilité des propriétés,
private
,protected
etpublic
, au niveau du trait.Si
public
n'est évidement pas un problème, pour les deux autres, ce n'est pas la même histoire,Bien évidement, j'ai un trunk sous le coude, mais pour ce que j'en sais, le code des traits n'est pas encore fonctionnel à 100% et il y a encore du travail au niveau de la résolution des conflits, et nous sommes en plein dedans,
Je suis cependant intéressé par tout retour sur le sujet, d'autant que je sais que tu vas creuser le sujet bien comme il faut.
Fait gaffe, tu vas dépasser ta limite de deux mois de PHP par an ;).6 De metagoto - 18/05/2010, 20:14
Je confirme ce que j'ai dit juste au dessus. Je viens de tester avec le dernier svn.
http://pastebin.com/YbkPkXcW
http://pastebin.com/pSGG6PbT
Pas besoins de getter/setter (ce n'est pas obligatoire). Les définitions sont mergées dans la classe host avec très peu de restrictions (les règles d'overloading des signatures).
Du coup, on peut bel et bien réaliser un nouvel idiome qui consiste à faire supporter à un trait l'implémentation d'une interface que la class host est censée implémenter.
A mon avis, "there is a need to describe the requirements a Trait will rely on" est superflu, tout du moins via l'intermédiaire de méthodes abstraites. Pourquoi ? Parce que tout se passe à la compilation. Ces requirements devraient être décrits dans la doc du trait en question mais pas formalisés dans le code. Ca ne sert à rien à part gonfler les tables des méthodes abstraites.
A mon sens, il vaut mieux construire des traits *volontairement dépendants* (dans les définitions) des classes hosts. Ca risque d'être un outils de choix pour les implémenteurs de librairies. Par exemple un trait qui manipule un $this->view. Ce $this->view semble sortir de nul part du point de vue du trait, mais tout prend son sens lorsque celui-ci se trouve mergé dans une classe host qui elle définit un $this->view (du genre BiduleController).
7 De mageekguy - 18/05/2010, 20:18
@metagoto : Bien noté.
L'inconvénient de cette façon de faire est qu'elle rend un trait très dépendant des classes qui l'utilise (spécialisation versus généricité).
Mais vu qu'apparament, et pour le moment, les deux sont possibles, libre au développeur de spécialiser son trait, au risque de devoir le dupliquer pour pouvoir l'utiliser dans une classe qui a le même besoin mais pas la même structure interne.
Je ne suis vraiment pas fan, vu que de mon point de vue, l'objectif premier des traits est de permettre de ne pas dupliquer du code, mais pourquoi pas.
Je sais que les méthodes abstraites et les getter/setter ne sont pas à la mode, mais pour le coup, dans ce contexte, j'en vois très bien l'intêret.
Par ailleurs, il y a encore pas mal de questions en suspend sur la gestion des traits, vis à vis de la reflexion (dans le sens reflectionClass, etc), du typage (est que instanceof doit permettre de valider qu'un trait est utilisé par une classe), etc.
Tout cela peut donc encore évoluer et je préfére jouer la prudence sur le sujet.
Et de toute façon, le fait de pouvoir manipuler une propriété d'une classe dans un trait n'a pas grand chose à voir avec les interfaces.
Une interface rend obligatoire l'implémentation dans une classe d'une méthode avec une signature spécifique.
Que cette dernière soit définie dans le corps de la classe ou dans un trait ne devrait donc pas poser de problème, vu que de toute façon les méthodes du trait sont dans la portée de la classe.
8 De metagoto - 18/05/2010, 21:41
Pour le respect des interfaces, il eu été possible que la vérification de la conformité de la classe au regard de(s) l'interface(s) se fasse *avant* le traitement des traits (et notamment le renommage avec le mot-clé as et les insteadof), ne rendant pas possible une bonne intégrations des traits dans le cadre d'interfaces. Heureusement, ça fonctionne bien (ils ont fait en sorte que ça soit le cas), mais ce n'était pas pour autant gagné d'avance.
Une interface, plusieurs implémentations sous forme de traits. Le tout est composé à la compilation (donc au niveau du source code). C'est la raison pour laquelle je me dis que du point de vue d'implémenteurs de libraires, ça peut être sympathique.
Et à propos d'utiliser une méthode abstraite vs. manipuler directement une property, le mieux est probablement de passer par une simple méthode privée.
9 De Olivier - 17/06/2010, 09:56
Cela devient désespérant. "Trait" présentait un véritable espoir de voir enfin apparaitre les "mixins" dans PHP et avec eux la promesse éternelle du code réutilisable et mutualisable. Hélas il n'en est rien, si l'on ne peut utiliser "$this" pour accéder à toutes les propriétés de la classe finale, c'est une nouvelle limite sur quelque chose que d'autres réalisent très simplement, je parle bien sur de Javascript.
L'implémentation des "Closures" est tout aussi mauvaise selon moi, et c'est bien dommage. Encore une fois Javascript est à des années lumière en terme de compétences objet alors que PHP s'embourbe.
10 De mageekguy - 17/06/2010, 12:43
@Olivier : Je pense que tu n'as pas suivi le dernier lien du billet.