Traits in PHP – any real world examples/best practices?

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?

59730 次浏览

我个人的观点是,在编写干净的代码时,对于 trait 的应用实际上非常少。

与其使用 trait 来破解类中的代码,不如通过构造函数或 setter 传递依赖关系:

class ClassName {
protected $logger;


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

我发现这比使用 trait 更好的主要原因是,通过消除与 trait 的硬耦合,您的代码更加灵活。例如,您现在可以简单地传递一个不同的日志记录器类。这使您的代码可重用和可测试。

我想人们需要研究一段时间具有 Traits 的语言来学习公认的良好/最佳实践。我目前对 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;
}

然后你做(小样)

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

我想在使用 trait 时需要考虑的重要事情是,它们实际上只是复制到类中的代码片段。这很容易导致冲突,例如,当您试图更改方法的可见性时,例如。

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

以上操作将导致错误(小样)。同样,在 trait 中声明的任何方法,如果已经在 using 类中声明,也不会被复制到类中,例如。

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


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

将印刷2(小样)。这些都是您想要避免的事情,因为它们会使错误难以发现。您还需要避免将内容放入对使用它的类的属性或方法进行操作的 trait 中,例如。

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

(小样) ,但是现在特征与 A 紧密耦合,并且水平重用的整个思想已经丢失。

当您遵循 接口隔离原则时,您将有许多小的类和接口。这使 Traits 成为你提到的事物的理想候选者,例如 横切关注点,但不是组成对象(在结构意义上)。在上面的 Logger 示例中,trait 是完全隔离的。它不依赖于具体的类。

我们可以使用 聚合/组合(如本页其他地方所示)来实现相同的结果类,但是使用聚合/组合的缺点是我们必须手动将代理/委托方法添加到应该能够记录日志的每个类中。Traits 很好地解决了这个问题,它允许我将样板文件放在一个地方,并在需要的地方选择性地应用它。

注意: 鉴于 trait 在 PHP 中是一个新的概念,以上表达的所有观点都可能发生变化。我自己还没有太多时间来评估这个概念。但我希望它足够好,让你有东西可以思考。

:)我不喜欢理论化和辩论什么应该做的东西。在这种情况下,特征。我将向你展示我发现的有用的特征,你可以从中学习,也可以忽略它。

特性 -应用 策略非常有用。简而言之,当您希望以不同的方式处理(筛选、排序等)相同的数据时,策略设计模式非常有用。

例如,您有一个基于某些标准(品牌、规格等等)或通过不同方式(价格、标签等等)进行排序的产品列表。您可以创建包含不同排序类型(数值、字符串、日期等)的不同函数的排序特性。然后,您不仅可以在您的产品类中使用这个 trait (如示例中所给出的) ,还可以在需要类似策略的其他类中使用(对某些数据应用数值排序等)。

试试看:

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

作为结束语,我考虑了一些特性,比如附件(我可以用它们来改变我的数据)。类似的方法和属性可以从我的类中删除,放在一个单独的地方,以便于维护,代码更短、更清晰。

我为 Traits 感到兴奋,因为他们在为 Magento 电子商务平台开发扩展时解决了 共同的问题。当扩展通过扩展向核心类(比如 User 模型)添加功能时,就会出现问题。这是通过指向 Zend autoloader (通过一个 XML 配置文件)使用扩展中的 User 模型来完成的,并让新模型扩展核心模型。但是如果两个扩展覆盖了同一个模型呢?您得到一个“比赛条件”,并且只有一个是加载的。

现在的解决方案是编辑扩展,以便一个扩展在链中扩展另一个扩展的模型覆盖类,然后设置扩展配置,以正确的顺序加载它们,这样继承链就可以工作了。

这个系统经常导致错误,在安装新扩展时需要检查冲突和编辑扩展。这是一个痛苦,并打破了升级过程。

我认为使用 Traits 将是一个很好的方法来完成同样的事情,而不用这个烦人的模型覆盖“比赛条件”。当然,如果多个 Traits 实现具有相同名称的方法,仍然可能存在冲突,但是我想象一个简单的名称空间约定可以在很大程度上解决这个问题。

我认为 Traits 对于为 Magento 这样的大型 PHP 软件包创建扩展/模块/插件是很有用的。

您可以拥有如下只读对象的 trait:

  trait ReadOnly{
protected $readonly = false;


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

您可以检测是否使用了 trait,并确定是否应该将该对象写入数据库、文件等。