L'utilisation des "Trait" en PHP

Voici un type de classe dont j'ai fais la découverte dans le cadre de mon boulot et dont je n'avais jamais compris le sens car c'est un sujet qui n'est pas abordé dans les formations ni dans les ressources qu'on trouve habituellement sur le net (ça parle plutôt des interfaces, les factory, les adaptateurs, etc...).

Pour être assez concis, un Trait est une classe qui va se comporter comme une classe qui va s'hériter, mais à la différence qu'il est possible d'utiliser plusieurs trait au sein d'une même classe.

Si je reprends l'exemple de php.net :

<?php

class Base {
    public function sayHello() {
        echo 'Hello ';
    }
}

trait SayWorld {
    public function sayHello() {
        parent::sayHello();
        echo 'World!';
    }
}

class MyHelloWorld extends Base {
    use SayWorld;
}

$o = new MyHelloWorld();
$o->sayHello();

Dans cet exemple, la classe MyHelloWorld hérite de la classe Base qui contient la méthode sayHello() qui va echo "Hello ". La classe MyHelloWorld possède également le trait SayWorld qui possède cette même méthode sayHello() mais va récupérer le contenu de la méthode sayHello() de la classe parente (Base) puis echo la sienne, ce qui va donner comme résultat Hello World! quand la méthode sera appelée dans la classe MyHelloWorld.

Il y a également une autre façon de le faire qui sera peut-être plus lisible :

<?php

class Base {
    public function sayHello() {
        echo 'Hello ';
    }
}

trait SayWorld {
    public function sayWorld() {
        echo 'World!';
    }
}

class MyHelloWorld extends Base {
    use SayWorld;
}

$o = new MyHelloWorld();
$o->sayHello() . $o->sayWorld();

Ce qui sera affiché sera la même chose sauf que cette fois j'ai séparé l'appel aux deux méthodes pour montrer que la méthode sayWorld() peut être appelée comme si elle faisait partie de la classe MyHelloWorld au même titre qu'une méthode d'une classe héritée.

On peut également voir un exemple qui est plus proche de la réalité en terme de code :

<?php

declare(strict_types=1);

namespace App\Entity;

class Product implements EntityInterface
{
    use AutoIdentifiableTrait;
    use UuidTrait;
    use NameableTrait;
    use TimestampableTrait;
    
    public function __construct(
    	private string $code,
        private float $price
    ) {
    }
    
    public function getCode(): string
    {
    	return $this->code;
    }
    
    public function setCode(string $code): self
    {
    	$this->code = $code;
        
        return $this;
    }
    
    public function getPrice(): float
    {
    	return $this->price;
    }
    
    public function setPrice(float $price): self
    {
    	$this->price = $price;
        
        return $this;
    }
}
App\Entity\Product.php

L'exemple est un peu long mais c'est nécessaire pour la suite. Donc la classe Product utilise quatre traits AutoIdentifiableTrait, UuidTrait, NameableTrait et TimestampableTrait. Ces traits ont le contenu suivant :

<?php

declare(strict_types=1);

namespace App\Trait\AutoIdentifiableTrait;

Trait AutoIdentifiableTrait {
    private readonly int $id;
}
App\Trait\AutoIdentifiableTrait.php
<?php

declare(strict_types=1);

namespace App\Trait\UuidTrait;

Trait UuidTrait
{
    private string $uuid;
    
    public function getUuid(): string
    {
    	return $this->uuid;
    }
    
    public function setUuid(string $uuid): self
    {
    	$this->uuid = $uuid;
        
        return $this;
    }
}
App\Trait\UuidTrait.php
<?php

declare(strict_types=1);

namespace App\Trait\NameableTrait;

Trait NameableTrait {
    private string $name;
    
    public function getName(): string
    {
    	return $this->name;
    }
    
    public function setName(string $name): self
    {
    	$this->name = $name;
        
        return $this;
    }
}
App\Trait\NameableTrait.php
<?php

declare(strict_types=1);

namespace App\Trait\Timestampable;

Trait Timestampable {
    private \DateTimeInterface $createdAt;
    
    private ?\DateTimeInterface $updatedAt;
    
    public function getCreatedAt(): \DateTimeInterface
    {
    	return $this->createdAt;
    }
    
    public function setName(\DateTimeInterface $createdAt): self
    {
    	$this->createdAt = $createdAt;
        
        return $this;
    }
    
    public function getUpdatedAt(): \DateTimeInterface
    {
    	return $this->createdAt;
    }
    
    public function setUpdatedAt(?\DateTimeInterface $updatedAt): self
    {
    	$this->updatedAt = $updatedAt;
        
        return $this;
    }
}
App\Trait\TimestampableTrait.php

Cela signifie que la classe Product aura les propriétés et les méthodes de tous les traits au sein de sa classe. Et vous pourrez utiliser n'importe lequel de ces traits dans une autre classe si vous le souhaitez.

Cependant si vous modifiez un trait, les changements seront répercutés dans toutes les classes utilisant le trait que vous aurez modifié.

Et là vous allez me demander ? "Mais quel est l'intérêt de faire un trait ?". Et la réponse est : ça vous permet de normaliser le format de vos propriétés à travers plusieurs classes par exemple car rien ne vous empêche d'utiliser les traits que nous venons de créer dans une classe User ou Book par exemple car cela a les avantages suivants :

  • La déclaration est unique, c'est à dire que si vous êtes une faute de frappe ou que vous devez modifier le contenu du trait à travers toutes les classes, il suffit de le faire qu'une seule fois dans celui-ci au lieu de chercher toutes les occurrences dans toutes les classes où c'est utilisé.
  • Cela permet d'être certain que ce qui est déclaré dans le trait l'est de manière consistante dans toutes les classes où il est utilisé.

Ensuite si vous avez un traitement particulier à faire qui ne colle pas à celle qui est dans le trait, il suffit de mettre le code dans la classe en question comme d'habitude et de ne pas importer le trait au sein de la classe.

J'espère que cela vous sera utile dans vos développements.