Traits in PHP – any real world examples/best pract

2020-01-26 13:14发布

问题:

As it currently stands, this question is not a good fit for our Q&A format. We expect answers to be supported by facts, references, or expertise, but this question will likely solicit debate, arguments, polling, or extended discussion. If you feel that this question can be improved and possibly reopened, visit the help center for guidance.
Closed 6 years ago.

Traits have been one of the biggest additions for PHP 5.4. I know the syntax and understand the idea behind traits, like horizontal code re-use for common stuff like logging, security, caching etc.

However, I still don't know how I would make use of traits in my projects.

Are there any open source projects that already use traits? Any good articles/reading material on how to structure architectures using traits?

回答1:

My personal opinion is that there is actually very little application for traits when writing clean code.

Instead of using traits to hack code into a class it is better to pass in the dependencies via the constructor or via setters:

class ClassName {
    protected $logger;

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

The main reason why I find that better than using traits is that your code is much more flexible by removing the hard coupling to a trait. For example you could simply pass a different logger class now. This makes your code reusable and testable.



回答2:

I guess one would have to look into languages that have Traits for some time now to learn the accepted Good/Best practices. My current opinion on Trait is that you should only use them for code that you would have to duplicate in other classes that share the same functionality.

Example for a Logger trait:

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;
}

And then you do (demo)

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

I guess the important thing to consider when using traits is that they really are just pieces of code that get copied into the class. This can easily lead to conflicts, for instance, when you try to change visibility of methods, e.g.

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

The above will result in an error (demo). Likewise, any methods declared in the trait that are also already declared in the using class will not get copied into the class, e.g.

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

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

will print 2 (demo). These are things you will want to avoid because they make errors hard to find. You will also want to avoid putting things into traits that operate on properties or methods of the class that uses it, e.g.

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();

works (demo) but now the trait is intimately coupled to A and the whole idea of horizontal reuse is lost.

When you follow the Interface Segregation Principle you will have many small classes and interfaces. That makes Traits an ideal candidate for the things you mentioned, e.g. crosscutting concerns, but not to compose objects (in a structual sense). In our Logger example above, the trait is completely isolated. It has no dependencies on concrete classes.

We could use aggregation/composition (like shown elsewhere on this page) to achieve the same resulting class, but the drawback of using aggregation/composition is that we will have to add the proxy/delegator methods manually to each and every class then that should be able to log. Traits solve this nicely by allowing me to keep the boilerplate in one place and selectively apply it where needed.

Note: given that traits are a new concept in PHP, all opinion expressed above is subject to change. I've not have had much time to evaluate the concept myself yet. But I hope it is good enough to give you something to think about.



回答3:

:) I don't like to theorize and debate about what should be done with something. In this case traits. I'll show you what I find traits useful for and you can either learn from it, or ignore it.

Traits - they are great to apply strategies. Strategy design patterns, in short, are useful when you want the same data to be handled (filtered, sorted, etc) differently.

For example, you have a list of products that you want to filter out based on some criteria (brands, specs, whatever), or sorted by different means (price, label, whatever). You can create a sorting trait that contains different functions for different sorting types (numeric, string, date, etc). You can then use this trait not only in your product class (as given in the example), but also in other classes that need similar strategies (to apply a numeric sort to some data, etc).

Try it:

<?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>';
?>

As a closing note, I think about traits like accessories (which I can use to alter my data). Similar methods and properties that can get cut out from my classes and be put into a single place, for easy maintenance, shorter and cleaner code.



回答4:

I am excited for Traits because they solve a common issue when developing extensions for the Magento ecommerce platform. The problem occurs when extensions add functionality to a core class (like say the User model) by extending it. This is done by pointing the Zend autoloader (via a XML config file) to use the User model from the extension, and have that new model extend the core model. (example) But what if two extensions override the same model? You get a "race condition" and only one is loaded.

The solution right now is to edit the extensions so one extends the other's model override class in a chain, and then set the extension configuration to load them in the correct order so the inheritance chain works.

This system frequently causes errors, and when installing new extensions it's necessary to check for conflicts and edit extensions. This is a pain, and breaks the upgrade process.

I think using Traits would be a good way to accomplish the same thing without this annoying model override "race condition". Granted there could still be conflicts if multiple Traits implement methods with the same names, but I would imagine something like a simple namespace convention could solve this for the most part.

TL;DR I think Traits could be useful for creating extensions/modules/plugins for large PHP software packages like Magento.



回答5:

You could have a trait for read-only object like this:

  trait ReadOnly{  
      protected $readonly = false;

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

You could detect if that trait is used and determine wheter or not you should write that object in a database, file, etc.