Recherche…


Traits pour faciliter la réutilisation du code horizontal

Disons que nous avons une interface pour la journalisation:

interface Logger {
    function log($message);
}

Maintenant, disons que nous avons deux implémentations concrètes de l'interface Logger : FileLogger et ConsoleLogger .

class FileLogger implements Logger {
    public function log($message) {
        // Append log message to some file
    }
}

class ConsoleLogger implements Logger {
    public function log($message) {
        // Log message to the console
    }
}

Maintenant, si vous définissez une autre classe Foo que vous voulez également pouvoir exécuter des tâches de journalisation, vous pouvez faire quelque chose comme ceci:

class Foo implements Logger {
    private $logger;

    public function setLogger(Logger $logger) {
        $this->logger = $logger;
    }

    public function log($message) {
        if ($this->logger) {
            $this->logger->log($message);
        }
    }
}

Foo est maintenant un Logger , mais sa fonctionnalité dépend de l'implémentation de Logger via setLogger() . Si nous voulons maintenant que class Bar également ce mécanisme de journalisation, nous devrions dupliquer ce morceau de logique dans la classe Bar .

Au lieu de dupliquer le code, un trait peut être défini:

trait LoggableTrait {
    protected $logger;

    public function setLogger(Logger $logger) {
        $this->logger = $logger;
    }

    public function log($message) {
        if ($this->logger) {
            $this->logger->log($message);
        }
    }
}

Maintenant que nous avons défini la logique dans un trait, nous pouvons utiliser le trait pour ajouter la logique aux classes Foo et Bar :

class Foo {
    use LoggableTrait;
}

class Bar {
    use LoggableTrait;
}

Et, par exemple, nous pouvons utiliser la classe Foo comme ceci:

$foo = new Foo();
$foo->setLogger( new FileLogger() );

//note how we use the trait as a 'proxy' to call the Logger's log method on the Foo instance
$foo->log('my beautiful message'); 

Résolution de conflit

Essayer d'utiliser plusieurs traits dans une classe peut entraîner des problèmes de méthodes contradictoires. Vous devez résoudre ces conflits manuellement.

Par exemple, créons cette hiérarchie:

trait MeowTrait {
    public function say() {
        print "Meow \n";
    }
}

trait WoofTrait {
    public function say() {
        print "Woof \n";
    }
}

abstract class UnMuteAnimals {
    abstract function say();
}

class Dog extends UnMuteAnimals {
    use WoofTrait;
}

class Cat extends UnMuteAnimals {
    use MeowTrait;
}

Maintenant, essayons de créer la classe suivante:

class TalkingParrot extends UnMuteAnimals {
    use MeowTrait, WoofTrait;
}

L'interprète php renverra une erreur fatale:

Erreur fatale: la méthode Trait n'a pas été appliquée, car il existe des collisions avec d'autres méthodes de trait sur TalkingParrot

Pour résoudre ce conflit, nous pourrions faire ceci:

  • utiliser le mot-clé au insteadof pour utiliser la méthode d'un trait au lieu de la méthode d'un autre trait
  • créer un alias pour la méthode avec une construction comme WoofTrait::say as sayAsDog;
class TalkingParrotV2 extends UnMuteAnimals {
    use MeowTrait, WoofTrait {
        MeowTrait::say insteadof WoofTrait;
        WoofTrait::say as sayAsDog;
    }
}

$talkingParrot = new TalkingParrotV2();
$talkingParrot->say();
$talkingParrot->sayAsDog();

Ce code produira la sortie suivante:

Miaou
Trame

Utilisation de plusieurs caractéristiques

trait Hello {
    public function sayHello() {
        echo 'Hello ';
    }
}

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

class MyHelloWorld {
    use Hello, World;
    public function sayExclamationMark() {
        echo '!';
    }
}

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

L'exemple ci-dessus affichera:

Hello World!

Changer la visibilité de la méthode

trait HelloWorld {
    public function sayHello() {
        echo 'Hello World!';
    }
}

// Change visibility of sayHello
class MyClass1 {
    use HelloWorld { sayHello as protected; }
}

// Alias method with changed visibility
// sayHello visibility not changed
class MyClass2 {
    use HelloWorld { sayHello as private myPrivateHello; }
}

Exécuter cet exemple:

(new MyClass1())->sayHello();
// Fatal error: Uncaught Error: Call to protected method MyClass1::sayHello()

(new MyClass2())->myPrivateHello();
// Fatal error: Uncaught Error: Call to private method MyClass2::myPrivateHello()

(new MyClass2())->sayHello();
// Hello World!

MyClass2 donc que dans le dernier exemple de MyClass2 la MyClass2 originale sans alias de trait HelloWorld reste accessible telle MyClass2 .

Qu'est-ce qu'un trait?

PHP n'autorise qu'un seul héritage. En d'autres termes, une classe ne peut que extend une autre classe. Mais que faire si vous devez inclure quelque chose qui n'appartient pas à la classe parente? Avant PHP 5.4, vous deviez être créatif, mais dans 5.4 Traits ont été introduits. Les traits vous permettent essentiellement de "copier et coller" une partie d'une classe dans votre classe principale

trait Talk {
    /** @var string */
    public $phrase = 'Well Wilbur...';
    public function speak() {
         echo $this->phrase;
    }
}

class MrEd extends Horse {
    use Talk;
    public function __construct() {
         $this->speak();
    }

    public function setPhrase($phrase) {
         $this->phrase = $phrase;
    }
}

Nous avons donc ici MrEd , qui étend déjà Horse . Mais pas tous les chevaux Talk , nous avons donc un trait pour cela. Notons ce que cela fait

Tout d'abord, nous définissons notre Trait. Nous pouvons l'utiliser avec le chargement automatique et les espaces de noms (voir aussi Référencement d'une classe ou d'une fonction dans un espace de noms ). Ensuite, nous l'incluons dans notre classe MrEd avec le mot-clé use .

Vous noterez que MrEd utilise les fonctions et les variables Talk sans les définir. Rappelez-vous ce que nous avons dit à propos de copier et coller ? Ces fonctions et variables sont toutes définies dans la classe maintenant, comme si cette classe les avait définies.

Les traits sont plus étroitement liés aux classes abstraites dans la mesure où vous pouvez définir des variables et des fonctions. Vous ne pouvez pas non plus instancier directement un Trait (c.-à-d. Un new Trait() ). Les traits ne peuvent pas forcer une classe à définir implicitement une fonction comme une classe abstraite ou une interface. Les traits ne sont que des définitions explicites (puisque vous pouvez implement autant d'interfaces que vous le souhaitez, voir Interfaces ).

Quand devrais-je utiliser un trait?

Lors de l'examen d'un trait, la première chose à faire est de vous poser cette question importante.

Puis-je éviter d'utiliser un Trait en restructurant mon code?

Le plus souvent, la réponse sera oui . Les traits sont des cas marginaux causés par un seul héritage. La tentation d’abuser ou d’abuser des caractéristiques peut être élevée. Mais considérez qu'un Trait introduit une autre source pour votre code, ce qui signifie qu'il y a une autre couche de complexité. Dans l'exemple ici, nous ne traitons que de 3 classes. Mais les Traits signifient que vous pouvez maintenant traiter beaucoup plus que cela. Pour chaque trait, votre classe devient beaucoup plus difficile à gérer, puisque vous devez maintenant faire référence à chaque trait pour savoir ce qu'il définit (et éventuellement une collision, voir Résolution de conflits ). Idéalement, vous devriez conserver le moins possible de traits dans votre code.

Traits pour garder les classes propres

Au fil du temps, nos classes peuvent implémenter de plus en plus d'interfaces. Lorsque ces interfaces ont plusieurs méthodes, le nombre total de méthodes de notre classe deviendra très important.

Par exemple, supposons que nous ayons deux interfaces et une classe les implémentant:

interface Printable {
    public function print();   
    //other interface methods...
}

interface Cacheable {
    //interface methods
}

class Article implements Cachable, Printable {  
    //here we must implement all the interface methods
    public function print(){ {
        /* code to print the article */ 
    }
}

Au lieu d'implémenter toutes les méthodes d'interface dans la classe Article , nous pourrions utiliser des Traits distincts pour implémenter ces interfaces, en gardant la classe plus petite et en séparant le code de l'implémentation de l'interface de la classe.

Par exemple, pour implémenter l'interface Printable , nous pourrions créer cette caractéristique:

trait PrintableArticle {
    //implements here the interface methods
    public function print() {
        /* code to print the article */ 
    }
}

et faire en sorte que la classe utilise le trait:

class Article implements Cachable, Printable {
    use PrintableArticle;
    use CacheableArticle; 
} 

Les principaux avantages seraient que nos méthodes d’implémentation d’interface seront séparées du reste de la classe et stockées dans un trait qui est seul responsable de l’implémentation de l’interface pour ce type d’objet.

Implémentation d'un singleton à l'aide de traits

Disclaimer : En aucun cas cet exemple ne préconise l'utilisation de singletons. Les singletons doivent être utilisés avec beaucoup de soin.

En PHP, il existe un moyen standard d'implémenter un singleton:

public class Singleton {
    private $instance;

    private function __construct() { };

    public function getInstance() {
        if (!self::$instance) {
            // new self() is 'basically' equivalent to new Singleton()
            self::$instance = new self();
        }

        return self::$instance;
    }

    // Prevent cloning of the instance
    protected function __clone() { }

    // Prevent serialization of the instance
    protected function __sleep() { }

    // Prevent deserialization of the instance
    protected function __wakeup() { }
}

Pour éviter la duplication de code, il est conseillé d’extraire ce comportement dans un trait.

trait SingletonTrait {
    private $instance;

    protected function __construct() { };

    public function getInstance() {
        if (!self::$instance) {
            // new self() will refer to the class that uses the trait
            self::$instance = new self();
        }

        return self::$instance;
    }

    protected function __clone() { }
    protected function __sleep() { }
    protected function __wakeup() { }
}

Désormais, toute classe souhaitant fonctionner en singleton peut simplement utiliser le trait:

class MyClass {
    use SingletonTrait;
}

// Error! Constructor is not publicly accessible
$myClass = new MyClass();

$myClass = MyClass::getInstance();

// All calls below will fail due to method visibility
$myClassCopy = clone $myClass; // Error!
$serializedMyClass = serialize($myClass); // Error!
$myClass = deserialize($serializedMyclass); // Error!

Même s'il est maintenant impossible de sérialiser un singleton, il est toujours utile d'interdire la méthode de désérialisation.



Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow