Les traits en PHP

mis en ligne le 04/12/2014

Les traits en PHP

Les traits en PHP

Comme nous l’avons vu dans l’article La POO en PHP en 10 minutes (ou moins), l’héritage est un moyen simple de réutiliser des comportements existants et donc d’éviter la dupplication de code. C’est d’ailleurs un pilier fondamental de la programmation orientée objet.

En revanche, la relation d’héritage est en fait une spécialisation. Ce qui signifie que la classe qui hérite doit partager une nature commune avec sa mère; par exemple, la classe Voiture peut être une spécialisation de la classe Vehicule mais certainement pas de la classe Canapé. Pourtant, dans le canapé comme dans la voiture on peut s’asseoir à plusieurs. Comment faire dans ce cas pour implémenter la méthode faireAsseoir(Personne $personne) dans deux classes qui ne partagent pas du tout les mêmes classes de base et ne peuvent pas s’hériter entre-elles ?

Jusqu’ici, la solution à ce genre de problème était de soit:

  • duppliquer le code (et tant pis pour les bonnes pratiques)
  • réaliser une composition (par exemple en injectant une instance de Fauteuil, ce qui introduit du couplage et de la complexité)

L’un comme l’autre peut poser problème. C’est pourquoi il existe depuis PHP 5.4 la notion de traits.

Encore un type de classe ?

Oui et non. Un trait est une structure qui rassemble les avantages d’une classe et d’une interface:

  • comme une classe, un trait peut avoir des méthode définies (et pas seulement déclarées)
  • comme une classe, un trait peut avoir des propriétés (contrairement aux interfaces)
  • comme une interface, on peut utiliser plusieurs traits dans une même classe
  • comme une interface, un trait ne peut être instancé

Un trait caractérise en quelque sorte un hértiage horizontal, indépendant de la hiérachie des types. Pour utiliser un trait, il n’est pas nécéssaire que celui-ci partage une nature commune avec la classe qui l’utilise. On peut le voir en fait comme un “morceau” de classe fournissant des fonctionnalités indépendantes de tout typage.

Pour ceux qui travaillent avec JS ou Ruby, on retrouve avec les traits la notion de mixin, c’est à dire de la fabrication de classes en “mélangeant” des objets. Par ailleurs, les traits existent aussi en Scala et bientôt en SmallTalk.

A quoi ça ressemble ?

Prennons un exemple utile: l’EventEmitter. Il s’agit de permettre à un objet d’émettre des évènements, par exemple lors d’un changement de son état, qui seront capturés par les écouteurs. Avant les traits on devait disposer une classe dédiée et passer ses instances aux classes susceptibles d’émettre des évènements, maintenant on peut tout simplement créer un jeu de méthodes qui feront exactement la même chose:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

trait EventEmitter
{
	protected $listeners;

	public function on($event, Closure $listner)
	{
		$this->listeners[$event][] = $listener->bindTo($this);
	}

	public function emit($event, array $data = [])
	{
		if (!isset($this->listeners[$event]))
			return;

		foreach ($this->listeners[$event] as $listener)
			$listener($data);
	}
}

?>

Pour utiliser un trait au sein d’une classe, on utilise le mot clé use:

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
<?php

class Personne
{
	use EventEmitter;

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

	public function naitre($date)
	{
		$this->emit('naissance', [$date]);
	}

	public function mourrir($date)
	{
		$this->emit('mort', [$date]);
	}
}

$p = new Personne("Denis Ritchie");
$p->on("naissance", function($data) {
	echo "{$this->name} est néé le {$data[0]}";
});
$p->on("mort", function($data) {
	echo "{$this->name} est mort le {$data[0]}";
});

$p->naitre('9 Septembre 1941');
$p->mourrir('12 Octobre 2011');

?>

Et qu’est-ce qu’on peut mettre dans un trait ?

Presque tout ce qu’on peut mettre dans une classe en réalité: des membres statiques, protégés, ou abstrait. On peut même utiliser des traits dans les traits

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php

trait Foo
{
	// un autre trait
	use Bar;

	// une propriété protégée
	protected $a;

	// une propriété statique
	public static $b = 2;

	// une méthode abstraite
	abstract public function c();

	// une méthode statique, protégée et abstraite
	abstract protected static function e();

	// etc.
}

?>

La classe qui utilise le trait peut à son tour changer le nom et la visibilité des méthodes du trait. Pour cela, on utilise la syntaxe suivante:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

class Foo
{
	// un seul trait utilisé
	use Bar {
		uneMethodePublique as protected unAutreNomDeMethode;
	}
}

class Baz
{
	// plusieurs traits utilisés
	use A, B {
		A::hello as traitHello;
		B::world as traitWorld;
	}
}

?>

En revanche, on ne peut pas changer le nom des propriétés. Avoir la même propriété dans la classe et dans le trait résulte soit d’une erreur stricte (E_STRICT) si les propriétés sont compatibles, soit d’une erreur fatale (E_FATAL_ERROR).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

trait Foo
{
	public $a = true;
	public $b = false;
}

class Bar
{
	use Foo;
	public $a = true; // E_STRICT
	public $b = true; // E_FATAL_ERROR !
}

?>

Priorisation

Les traits surchargent les méthodes de la classe parente mais les méthodes du trait sont surchargées par les méthodes déjà définies dans la classe. En fait c’est comme si le trait était “entre” la classe parente et la classe au niveau de la hiérarchie d’héritage.

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
<?php

class A
{
	public function foo() { echo 1; }
}

trait B
{
	public function foo() { echo 2; }
}

class C extends A
{
	use B;
}

$c = new C;
$c->foo(); // affiche 2 car A::foo est écrasée par B::foo

class D extends A
{
	use B;

	public function foo() { echo 3; }
}

$d = new D;
$d->foo(); // affiche 3 car D::foo écrase B::foo (qui écrase A::foo)

?>

Collisions

Quand une classe utilise plusieurs traits, des colisions de noms de méthodes peuvent se produire. Pour résoudre le problème, PHP 5.4 introduit un nouvel opérateur: insteadof pour signifier “utilise celle-ci plutôt que celle-là”. Changer manuellement le nom d’une méthode reste cependant possible. Il est important de noter que si le conflit n’est pas explicitement résolu, une erreur fatale (E_FATAL_ERROR) est émise.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php

trait Foo
{
	public function hello() { echo "Hello"; }
}

trait Bar
{
	public function hello() { echo "World"; }
}

class FooBar
{
	use Foo, Bar {
		Foo::hello insteadof Bar; // utiliser la méthode de Foo plutôt que celle de Bar
		Bar::hello as world;      // changer le nom de la méthode de Bar
	}

	// la classe dispose des méthode hello() et world()
}

?>

Conclusion

En résumé les traits sont utiles pour:

  • rassembler des comportements sans devoir créer des classes fourre-tout
  • partager des comportements entre des classes qui n’ont aucun lien entre-elles
  • fournir des bases d’implémentation concrètes pour des interfaces
  • limiter le trop grand sous typage des classes
  • limiter le nombre d’instances à créer par rapport à des compositions

Selon moi, le trait n’est pas une alternative à l’injection de dépendance mais un complément. L’injection de dépendance, parce qu’elle suppose que les fonctionnalités soient isolées dans des composants séparés, encourage la création de petits composants sans réelle valeur ajoutée à encapsuler. Les traits permettent de réduire le nombre de ces composants (et donc le nombre de classes) sans pour autant sacrifier à l’encapsulation ni à la séparation des responsabilités. L’exemple de l’EventEmitter décrit plus haut en est une parfaite illustration.

Coté performances, je n’ai pas encore pensé à regarder l’impact de l’utilisation d’un trait, je ne pense pas qu’il soit très important mais si quelqu’un dispose des chiffres n’hésitez pas à les partager dans les commentaires.

Commentaires

Système de commentaires propulsé par Disqus