Traits en PHP - des exemples du monde réel / des meilleures pratiques? [fermé]

148

Les traits ont été l'un des plus gros ajouts pour PHP 5.4. Je connais la syntaxe et comprends l'idée derrière les traits, comme la réutilisation horizontale du code pour des choses courantes comme la journalisation, la sécurité, la mise en cache, etc.

Cependant, je ne sais toujours pas comment j'utiliserais des traits dans mes projets.

Existe-t-il des projets open source qui utilisent déjà des traits? Y a-t-il de bons articles / lectures sur la façon de structurer des architectures à l'aide de traits?

Max
la source
8
Voici mon avis: un article de blog sur le sujet que j'ai écrit sur le sujet. TL; DR: Fondamentalement, je crains que bien qu'ils soient puissants et puissent être utilisés pour de bon, la majorité des utilisations que nous verrons seront des anti-modèles complets et causeront beaucoup plus de douleur qu'ils n'en résolvent ...
ircmaxell
1
Jetez un œil à la bibliothèque standard scala et vous trouverez de nombreux exemples utiles de traits.
dmitry

Réponses:

89

Mon opinion personnelle est qu'il y a en fait très peu d'applications pour les traits lors de l'écriture de code propre.

Au lieu d'utiliser des traits pour pirater du code dans une classe, il est préférable de passer les dépendances via le constructeur ou via des setters:

class ClassName {
    protected $logger;

    public function __construct(LoggerInterface $logger) {
        $this->logger = $logger;
    }
    // or
    public function setLogger(LoggerInterface $logger) {
        $this->logger = $logger;
    }
}

La principale raison pour laquelle je trouve cela meilleur que l'utilisation de traits est que votre code est beaucoup plus flexible en supprimant le couplage dur à un trait. Par exemple, vous pouvez simplement passer une classe d'enregistrement différente maintenant. Cela rend votre code réutilisable et testable.

NikiC
la source
4
En utilisant des traits, vous pouvez également utiliser une autre classe de logger, n'est-ce pas? Modifiez simplement le trait, et toutes les classes qui utilisent le trait seront mises à jour. Corrigez-moi si je me trompe
rickchristie
14
@rickchristie Bien sûr, vous pouvez le faire. Mais vous devrez modifier le code source du trait. Donc, vous le changeriez pour chaque classe qui l'utilise, pas seulement pour celle pour laquelle vous voulez un enregistreur différent. Et si vous souhaitez utiliser la même classe mais avec deux enregistreurs différents? Ou si vous voulez réussir un simulacre d'enregistrement pendant le test? Vous ne pouvez pas, si vous utilisez des traits, vous pouvez, si vous utilisez l'injection de dépendances.
NikiC
2
Je peux comprendre votre point de vue, je me demande également si les traits en valent la peine ou non. Je veux dire, dans les frameworks modernes comme Symfony 2, vous avez une injection de dépendances partout, ce qui semble supérieur aux traits dans la plupart des cas. Pour le moment, je ne vois pas beaucoup plus de traits que de "copier-coller assisté par compilateur". ;)
Max
11
Pour le moment, je ne vois pas beaucoup plus de traits que de "copier-coller assisté par compilateur". ;) : @Max: C'est exactement ce pour quoi les traits ont été conçus, donc c'est tout à fait correct. Cela le rend plus "maintenable", car il n'y a qu'une seule définition, mais c'est fondamentalement juste c & p ...
ircmaxell
29
NikiC manque le point: l'utilisation d'un trait n'empêche pas d'utiliser l'injection de dépendances. Dans ce cas, un trait permettrait simplement à chaque classe qui implémente la journalisation de ne pas avoir à dupliquer la méthode setLogger () et la création de la propriété $ logger. Le trait les fournirait. setLogger () taperait un indice sur LoggerInterface comme le fait l'exemple, de sorte que n'importe quel type d'enregistreur puisse être transmis. Cette idée est similaire à la réponse de Gordon ci-dessous (seulement il semble qu'il a tapé sur une super-classe Logger plutôt qu'une interface Logger ).
Ethan
205

Je suppose qu'il faudrait se pencher sur les langues qui ont des traits depuis un certain temps maintenant pour apprendre les bonnes / meilleures pratiques acceptées. Mon opinion actuelle sur Trait est que vous ne devriez les utiliser que pour du code que vous auriez à dupliquer dans d'autres classes partageant les mêmes fonctionnalités.

Exemple de trait Logger:

interface Logger
{
    public function log($message, $level);    
}

class DemoLogger implements Logger
{
    public function log($message, $level)
    {
        echo "Logged message: $message with level $level", PHP_EOL; 
    }
}

trait Loggable // implements Logger
{
    protected $logger;
    public function setLogger(Logger $logger)
    {
        $this->logger = $logger;
    }
    public function log($message, $level)
    {
        $this->logger->log($message, $level);
    }
}

class Foo implements Logger
{
    use Loggable;
}

Et puis tu fais ( démo )

$foo = new Foo;
$foo->setLogger(new DemoLogger);
$foo->log('It works', 1);

Je suppose que la chose importante à considérer lors de l'utilisation de traits est qu'ils ne sont en réalité que des morceaux de code qui sont copiés dans la classe. Cela peut facilement conduire à des conflits, par exemple, lorsque vous essayez de modifier la visibilité des méthodes, par exemple

trait T {
    protected function foo() {}
}
class A { 
    public function foo() {}
}
class B extends A
{
    use T;
}

Ce qui précède entraînera une erreur ( démo ). De même, toutes les méthodes déclarées dans le trait qui sont également déjà déclarées dans la classe using ne seront pas copiées dans la classe, par exemple

trait T {
    public function foo() {
    return 1;
}
}
class A { 
    use T;
    public function foo() {
    return 2;
}
}

$a = new A;
echo $a->foo();

imprimera 2 ( démo ). Ce sont des choses que vous voudrez éviter car elles rendent les erreurs difficiles à trouver. Vous voudrez également éviter de mettre des choses dans des traits qui opèrent sur des propriétés ou des méthodes de la classe qui l'utilise, par exemple

class A
{
    use T;
    protected $prop = 1;
    protected function getProp() {
        return $this->prop;
    }
}

trait T
{
    public function foo()
    {
        return $this->getProp();
    }
}

$a = new A;
echo $a->foo();

fonctionne ( démo ) mais maintenant le trait est intimement lié à A et toute l'idée de réutilisation horizontale est perdue.

Lorsque vous suivez le principe de séparation des interfaces, vous aurez de nombreuses petites classes et interfaces. Cela fait de Traits un candidat idéal pour les choses que vous avez mentionnées, par exemple les préoccupations transversales , mais pas pour composer des objets (dans un sens structuré). Dans notre exemple Logger ci-dessus, le trait est complètement isolé. Il n'a pas de dépendances sur les classes concrètes.

Nous pourrions utiliser l' agrégation / composition (comme indiqué ailleurs sur cette page) pour obtenir la même classe résultante, mais l'inconvénient d'utiliser l'agrégation / composition est que nous devrons ajouter manuellement les méthodes proxy / délégant à chaque classe, alors cela devrait être en mesure de se connecter. Les traits résolvent bien ce problème en me permettant de garder le passe-partout au même endroit et de l'appliquer de manière sélective si nécessaire.

Remarque: étant donné que les traits sont un nouveau concept en PHP, toutes les opinions exprimées ci-dessus sont susceptibles de changer. Je n'ai pas encore eu beaucoup de temps pour évaluer moi-même le concept. Mais j'espère que c'est assez bon pour vous donner quelque chose à penser.

Gordon
la source
41
C'est un cas d'utilisation intéressant: utilisez une interface qui définit le contrat, utilisez le trait pour satisfaire ce contrat. Bon.
Max
13
J'aime ce genre de vrais programmeurs, qui proposent de vrais exemples de travail avec une courte description pour chacun. Thx
Arthur Kushman
1
Et si quelqu'un utilise plutôt une classe abstraite? En remplaçant l'interface et le trait, on peut créer une classe abstraite. De plus, si l'interface est si nécessaire pour l'application, la classe abstraite peut également implémenter l'interface et définir les méthodes comme le faisait trait. Alors, pouvez-vous expliquer pourquoi nous avons encore besoin de traits?
sumanchalki
12
@sumanchalki La classe abstraite suit les règles de l'héritage. Et si vous aviez besoin d'une classe qui implémente Loggable et Cacheable? Vous auriez besoin de la classe pour étendre AbstractLogger qui doit alors étendre AbstractCache. Mais cela signifie que tous les Loggables sont des caches. C'est un couplage dont vous ne voulez pas. Cela limite la réutilisation et gâche votre graphe d'héritage.
Gordon
1
Je pense que les liens de démonstration sont morts
Pmpr
19

:) Je n'aime pas théoriser et débattre de ce qu'il faut faire avec quelque chose. Dans ce cas, les traits. Je vais vous montrer à quoi je trouve utile les traits et vous pouvez soit en tirer des leçons, soit les ignorer.

Traits - ils sont parfaits pour appliquer des stratégies . Les modèles de conception de stratégie, en bref, sont utiles lorsque vous souhaitez que les mêmes données soient traitées (filtrées, triées, etc.) différemment.

Par exemple, vous avez une liste de produits que vous souhaitez filtrer en fonction de certains critères (marques, spécifications, peu importe), ou triés par différents moyens (prix, étiquette, peu importe). Vous pouvez créer un trait de tri qui contient différentes fonctions pour différents types de tri (numérique, chaîne, date, etc.). Vous pouvez ensuite utiliser cette caractéristique non seulement dans votre classe de produit (comme indiqué dans l'exemple), mais également dans d'autres classes qui nécessitent des stratégies similaires (pour appliquer un tri numérique à certaines données, etc.).

Essayez-le:

<?php
trait SortStrategy {
    private $sort_field = null;
    private function string_asc($item1, $item2) {
        return strnatcmp($item1[$this->sort_field], $item2[$this->sort_field]);
    }
    private function string_desc($item1, $item2) {
        return strnatcmp($item2[$this->sort_field], $item1[$this->sort_field]);
    }
    private function num_asc($item1, $item2) {
        if ($item1[$this->sort_field] == $item2[$this->sort_field]) return 0;
        return ($item1[$this->sort_field] < $item2[$this->sort_field] ? -1 : 1 );
    }
    private function num_desc($item1, $item2) {
        if ($item1[$this->sort_field] == $item2[$this->sort_field]) return 0;
        return ($item1[$this->sort_field] > $item2[$this->sort_field] ? -1 : 1 );
    }
    private function date_asc($item1, $item2) {
        $date1 = intval(str_replace('-', '', $item1[$this->sort_field]));
        $date2 = intval(str_replace('-', '', $item2[$this->sort_field]));
        if ($date1 == $date2) return 0;
        return ($date1 < $date2 ? -1 : 1 );
    }
    private function date_desc($item1, $item2) {
        $date1 = intval(str_replace('-', '', $item1[$this->sort_field]));
        $date2 = intval(str_replace('-', '', $item2[$this->sort_field]));
        if ($date1 == $date2) return 0;
        return ($date1 > $date2 ? -1 : 1 );
    }
}

class Product {
    public $data = array();

    use SortStrategy;

    public function get() {
        // do something to get the data, for this ex. I just included an array
        $this->data = array(
            101222 => array('label' => 'Awesome product', 'price' => 10.50, 'date_added' => '2012-02-01'),
            101232 => array('label' => 'Not so awesome product', 'price' => 5.20, 'date_added' => '2012-03-20'),
            101241 => array('label' => 'Pretty neat product', 'price' => 9.65, 'date_added' => '2012-04-15'),
            101256 => array('label' => 'Freakishly cool product', 'price' => 12.55, 'date_added' => '2012-01-11'),
            101219 => array('label' => 'Meh product', 'price' => 3.69, 'date_added' => '2012-06-11'),
        );
    }

    public function sort_by($by = 'price', $type = 'asc') {
        if (!preg_match('/^(asc|desc)$/', $type)) $type = 'asc';
        switch ($by) {
            case 'name':
                $this->sort_field = 'label';
                uasort($this->data, array('Product', 'string_'.$type));
            break;
            case 'date':
                $this->sort_field = 'date_added';
                uasort($this->data, array('Product', 'date_'.$type));
            break;
            default:
                $this->sort_field = 'price';
                uasort($this->data, array('Product', 'num_'.$type));
        }
    }
}

$product = new Product();
$product->get();
$product->sort_by('name');
echo '<pre>'.print_r($product->data, true).'</pre>';
?>

En guise de conclusion, je pense à des traits comme les accessoires (que je peux utiliser pour modifier mes données). Des méthodes et des propriétés similaires qui peuvent être supprimées de mes classes et mises en un seul endroit, pour une maintenance facile, un code plus court et plus propre.

D. Marti
la source
1
Bien que cela garde l'interface publique propre, l'interface interne peut devenir très complexe avec cela, surtout si vous l'étendez à d'autres choses, comme les couleurs par exemple. Je pense que des fonctions simples ou des méthodes statiques pour mieux ici.
Sebastian Mach
J'aime le terme strategies.
Rannie Ollit
4

Je suis enthousiasmé par Traits car ils résolvent un problème courant lors du développement d'extensions pour la plate-forme de commerce électronique Magento. Le problème se produit lorsque les extensions ajoutent des fonctionnalités à une classe principale (comme par exemple le modèle User) en l'étendant. Ceci est fait en pointant l'autochargeur Zend (via un fichier de configuration XML) pour utiliser le modèle User de l'extension, et faire étendre le nouveau modèle du modèle de base. ( exemple ) Mais que se passe-t-il si deux extensions remplacent le même modèle? Vous obtenez une "condition de concurrence" et une seule est chargée.

La solution à l'heure actuelle consiste à modifier les extensions afin que l'une étende la classe de remplacement de modèle de l'autre dans une chaîne, puis à définir la configuration d'extension pour les charger dans le bon ordre afin que la chaîne d'héritage fonctionne.

Ce système provoque fréquemment des erreurs et lors de l'installation de nouvelles extensions, il est nécessaire de vérifier les conflits et de modifier les extensions. C'est pénible et interrompt le processus de mise à niveau.

Je pense que l'utilisation de Traits serait un bon moyen d'accomplir la même chose sans cette «condition de course» ennuyeuse du modèle. Certes, il pourrait encore y avoir des conflits si plusieurs Traits implémentaient des méthodes avec les mêmes noms, mais j'imagine que quelque chose comme une simple convention d'espace de noms pourrait résoudre cela pour la plupart.

TL; DR Je pense que Traits pourrait être utile pour créer des extensions / modules / plugins pour de gros progiciels PHP comme Magento.

thaddeusmt
la source
0

Vous pourriez avoir un trait pour un objet en lecture seule comme celui-ci:

  trait ReadOnly{  
      protected $readonly = false;

      public function setReadonly($value){ $this->readonly = (bool)$value; }
      public function getReadonly($value){ return $this->readonly; }
  }

Vous pouvez détecter si ce trait est utilisé et déterminer si vous devez ou non écrire cet objet dans une base de données, un fichier, etc.

Nico
la source
Ainsi, la classe qui appellerait usece trait alors if($this -> getReadonly($value)); mais cela générerait une erreur si vous ne faisiez pas usece trait. Cet exemple est donc défectueux.
Luceos
Eh bien, vous devez d'abord vérifier si le trait est utilisé. Si le trait ReadOnly est défini sur un objet, vous pouvez alors vérifier s'il est en lecture seule ou non.
Nico
J'ai fait une preuve de concept générique pour un tel trait dans gist.github.com/gooh/4960073
Gordon
3
Vous devez déclarer une interface pour ReadOnly à cette fin
Michael Tsang