使用特性

如果你曾经做过任何C语言编程,你也许会熟悉宏。宏是一个预定义的代码块,它在指定的行处展开。类似地,特性可以包含代码块,这些代码块在PHP解释器指定的行处被复制并粘贴到一个类中。

如何做...

1.特性用关键字 trait 标识,可以包含属性和方法。你可能已经注意到,在检查之前具有 CountryListCustomerList 类的案例时,有重复的代码。在这个例子中,我们将重构这两个类,并将 list() 方法的功能移到Trait 中。请注意,两个类中的 list() 方法是一样的。

2.特性用于类与类之间代码重复使用的情况。但请注意,传统的创建抽象类并扩展它的方法可能比 traits 更可能有某些优势。特性不能用来确定继承的线路,而抽象父类可以用于这个目的。

3.现在,我们将 list() 复制到一个名为 ListTrait 的特性中:

trait ListTrait
{
  public function list()
  {
    $list = [];
    $sql  = sprintf('SELECT %s, %s FROM %s', 
      $this->key, $this->value, $this->table);
    $stmt = $this->connection->pdo->query($sql);
    while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
      $list[$item[$this->key]] = $item[$this->value];
    }
    return $list;
  }
}

4.然后,我们可以将 ListTrait 中的代码插入到新类 CountryListUsingTrait 中,如以下代码片段所示。 现在可以从此类中删除整个 list() 方法:

class CountryListUsingTrait implements ConnectionAwareInterface
{

  use ListTrait;

  protected $connection;
  protected $key   = 'iso3';
  protected $value = 'name';
  protected $table = 'iso_country_codes';

  public function setConnection(Connection $connection)
  {
    $this->connection = $connection;
  }

}

每当您有重复的代码进行更改时就会出现潜在的问题。 您可能会发现自己不得不进行过多的全局搜索和替换操作,或者剪切和粘贴代码,而结果往往是灾难性的。 特性是避免这种维护噩梦的好方法。

5.特性受命名空间影响。 在步骤1所示的示例中,如果将新的 CountryListUsingTrait 类放置在命名空间 Application\Generic 中,则我们还需要将 ListTrait 也一起移入该命名空间:

namespace Application\Generic;

use PDO;

trait ListTrait
{
  public function list()
  {
    // ...
  }
}

6. 特性中的方法将覆盖继承的方法。

7.在下面的例子中,您会注意到 setId() 方法的返回值在 Base 父类和 Test 特性之间有所不同。Customer 类继承自 Base,但也使用 Test。在这种情况下,特性中定义的方法将覆盖 Base 父类中定义的方法:

trait Test
{
  public function setId($id)
  {
    $obj = new stdClass();
    $obj->id = $id;
    $this->id = $obj;
  }
}

class Base
{
  protected $id;
  public function getId()
  {
    return $this->id;
  }
  public function setId($id)
  {
    $this->id = $id;
  }
}

class Customer extends Base
{
  use Test;
  protected $name;
  public function getName()
  {
    return $this->name;
  }
  public function setName($name)
  {
    $this->name = $name;
  }
}

在 PHP 5 中,traits 也可以覆盖属性。在 PHP 7 中,如果 trait 中的属性初始化为与父类不同的值,会产生一个致命的错误。

8. 在类中定义了与特性中一样的方法,则在引入特性的时候了类中的方法将覆盖特性中的方法。

9.在这个例子中,Test 特性定义了一个属性 $id 以及 getId()setId()方法。该特性还定义了 setName(),它与 Customer 类中定义的相同方法有冲突。在这种情况下,Customer类中直接定义的 setName()方法将覆盖特性中定义的 setName()

trait Test
{
  protected $id;
  public function getId()
  {
    return $this->id;
  }
  public function setId($id)
  {
    $this->id = $id;
  }
  public function setName($name)
  {
    $obj = new stdClass();
    $obj->name = $name;
    $this->name = $obj;
  }
}

class Customer
{
  use Test;
  protected $name;
  public function getName()
  {
    return $this->name;
  }
  public function setName($name)
  {
    $this->name = $name;
  }
}

10.当使用多个特性时,使用 insteadof 关键字来解决方法名冲突。结合使用 as 关键字来对方法名进行别名设置。

11.在这个例子中,有两个特性,IdTraitNameTrait。这两个特性都定义了一个 setKey() 方法,但是使用了不同的方式来表达key。Test 类使用了这两个特性。注意 insteadof 关键字,它允许我们区分冲突的方法。因此,当从 Test 类调用 setKey() 时,源将从 NameTrait 中抽取。此外,来自 IdTraitsetKey() 仍将可用,但用的是别名setKeyDate()

trait IdTrait
{
  protected $id;
  public $key;
  public function setId($id)
  {
    $this->id = $id;
  }
  public function setKey()
  {
    $this->key = date('YmdHis') 
    . sprintf('%04d', rand(0,9999));
  }
}

trait NameTrait
{
  protected $name;
  public $key;
  public function setName($name)
  {
    $this->name = $name;
  }
  public function setKey()
  {
    $this->key = unpack('H*', random_bytes(18))[1];
  }
}

class Test
{
  use IdTrait, NameTrait {
    NameTrait::setKey insteadof IdTrait;
    IdTrait::setKey as setKeyDate;
  }
}

如何运行...

从步骤1开始,您了解到特性用于出现重复代码的情况下。 您需要评估是否可以简单地定义一个基类并扩展它,或者使用特性是否更好地满足您的目的。 当在逻辑上不相关的类中看到代码重复时,特性将特别有用。

为了说明特性方法如何覆盖继承的方法,请将步骤7中提到的代码块复制到一个单独的文件 chap_04_oop_traits_override_inherited.php 中。 添加以下代码行:

$customer = new Customer();
$customer->setId(100);
$customer->setName('Fred');
var_dump($customer);

从输出中可以看到(下图所示),属性 $id 中存储了 stdClass() 的实例,这就是在特性中定义的行为:

为了说明直接定义的类方法如何覆盖特性方法,请将步骤9中提到的代码块复制到单独的文件 chap_04_oop_trait_methods_do_not_override_class_methods.php 中。 添加以下代码行:

$customer = new Customer();
$customer->setId(100);
$customer->setName('Fred');
var_dump($customer);

从以下输出中可以看到,$id 属性存储为整数,如在 Customer 类中定义的,而特性将 $id 定义为 stdClass 的实例:

在步骤10中,您学习了如何在使用多个特性时解决重复的方法名称冲突。 将步骤11中显示的代码块复制到单独的文件 chap_04_oop_trait_multiple.php 中。 添加以下代码:

$a = new Test();
$a->setId(100);
$a->setName('Fred');
$a->setKey();
var_dump($a);

$a->setKeyDate();
var_dump($a);

请注意,在下面的输出中,setKey() 返回的数据是PHP 7新函数random_bytes() (在 NameTrait中定义)产生的,而 setKeyDate() 返回的数据是使用 date()rand() 函数(在 IdTrait 中定义)产生的:

最后更新于