使用特性

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

如何做…

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

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

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

  1. trait ListTrait
  2. {
  3. public function list()
  4. {
  5. $list = [];
  6. $sql = sprintf('SELECT %s, %s FROM %s',
  7. $this->key, $this->value, $this->table);
  8. $stmt = $this->connection->pdo->query($sql);
  9. while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
  10. $list[$item[$this->key]] = $item[$this->value];
  11. }
  12. return $list;
  13. }
  14. }

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

  1. class CountryListUsingTrait implements ConnectionAwareInterface
  2. {
  3. use ListTrait;
  4. protected $connection;
  5. protected $key = 'iso3';
  6. protected $value = 'name';
  7. protected $table = 'iso_country_codes';
  8. public function setConnection(Connection $connection)
  9. {
  10. $this->connection = $connection;
  11. }
  12. }

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

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

  1. namespace Application\Generic;
  2. use PDO;
  3. trait ListTrait
  4. {
  5. public function list()
  6. {
  7. // ...
  8. }
  9. }
  1. 特性中的方法将覆盖继承的方法。

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

  1. trait Test
  2. {
  3. public function setId($id)
  4. {
  5. $obj = new stdClass();
  6. $obj->id = $id;
  7. $this->id = $obj;
  8. }
  9. }
  10. class Base
  11. {
  12. protected $id;
  13. public function getId()
  14. {
  15. return $this->id;
  16. }
  17. public function setId($id)
  18. {
  19. $this->id = $id;
  20. }
  21. }
  22. class Customer extends Base
  23. {
  24. use Test;
  25. protected $name;
  26. public function getName()
  27. {
  28. return $this->name;
  29. }
  30. public function setName($name)
  31. {
  32. $this->name = $name;
  33. }
  34. }

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

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

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

  1. trait Test
  2. {
  3. protected $id;
  4. public function getId()
  5. {
  6. return $this->id;
  7. }
  8. public function setId($id)
  9. {
  10. $this->id = $id;
  11. }
  12. public function setName($name)
  13. {
  14. $obj = new stdClass();
  15. $obj->name = $name;
  16. $this->name = $obj;
  17. }
  18. }
  19. class Customer
  20. {
  21. use Test;
  22. protected $name;
  23. public function getName()
  24. {
  25. return $this->name;
  26. }
  27. public function setName($name)
  28. {
  29. $this->name = $name;
  30. }
  31. }

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

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

  1. trait IdTrait
  2. {
  3. protected $id;
  4. public $key;
  5. public function setId($id)
  6. {
  7. $this->id = $id;
  8. }
  9. public function setKey()
  10. {
  11. $this->key = date('YmdHis')
  12. . sprintf('%04d', rand(0,9999));
  13. }
  14. }
  15. trait NameTrait
  16. {
  17. protected $name;
  18. public $key;
  19. public function setName($name)
  20. {
  21. $this->name = $name;
  22. }
  23. public function setKey()
  24. {
  25. $this->key = unpack('H*', random_bytes(18))[1];
  26. }
  27. }
  28. class Test
  29. {
  30. use IdTrait, NameTrait {
  31. NameTrait::setKey insteadof IdTrait;
  32. IdTrait::setKey as setKeyDate;
  33. }
  34. }

如何运行…

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

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

  1. $customer = new Customer();
  2. $customer->setId(100);
  3. $customer->setName('Fred');
  4. var_dump($customer);

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

使用特性 - 图1

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

  1. $customer = new Customer();
  2. $customer->setId(100);
  3. $customer->setName('Fred');
  4. var_dump($customer);

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

使用特性 - 图2

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

  1. $a = new Test();
  2. $a->setId(100);
  3. $a->setName('Fred');
  4. $a->setKey();
  5. var_dump($a);
  6. $a->setKeyDate();
  7. var_dump($a);

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

使用特性 - 图3