关于PHP中 Trait 的使用

最近遇到一个问题,我在 Trait 中写了个写了个 构造函数 然后妄图注入一个 Laravel服务 后面再使用这个 Trait 的时候发生服务的初始化报错(别问我为什么这么干,也是脑子瓦特了才能有这种骚操作…) 找了一圈发现是优先级的问题 顺带补了下 Trait 的使用


Trait 是什么

自 PHP 5.4.0 起,PHP 实现了一种代码复用的方法,称为 trait。

Trait 是为类似 PHP 的单继承语言而准备的一种代码复用机制。Trait 为了减少单继承语言的限制,使开发人员能够自由地在不同层次结构内独立的类中复用 method。Trait 和 Class 组合的语义定义了一种减少复杂性的方式,避免传统多继承和 Mixin 类相关典型问题。

Trait 和 Class 相似,但仅仅旨在用细粒度和一致的方式来组合功能。 无法通过 trait 自身来实例化。它为传统继承增加了水平特性的组合;也就是说,应用的几个 Class 之间不需要继承。

Trait 翻译过来就是:特性的意思。 他提供了另一种维度的代码复用 我的理解就是你在引入的时候就等于帮他你把 Trait 中的 Function 复制到了你当前类中 可以让你直接调用

优先级问题

对于我上面犯的错就是典型的优先级问题 官方说的是 优先顺序是当前类中的方法会覆盖 trait 方法,而 trait 方法又覆盖了基类中的方法

首先我定义了一个 trait

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
trait TestTrait
{
protected $service;
public function __construct(TestService $service)
{
$this->service = $service;
}
public function showHello()
{
$hello = $this->service->sayHello("reggie");
echo $hello;
}
}

然后在类中引入 trait 调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Test
{
use TestTrait;
public function __construct()
{
}
public function showTest()
{
try {
$this->showHello();
} catch (\Exception $exception) {
echo "have some exception:" . $exception->getMessage();
}
}
}

这个时候会有一个报错 Call to a member function sayHello() on null 这里其实就是优先级的问题 从本质上 你引用trait 就是把他里面的方法复制到了你当前类中, 而我当前类中有一个空的构造函数 就算你在trait中定义了构造(除了我这种脑抽的人应该也没有人会在taint中在定义构造函数了…),根据优先级顺序是当前类覆盖trait,所以trait使用的是当前类的空的构造函数 并不会使用他本身的那个带注入 TestService 的构造函数 所以导致在 showHello 方法中 $this->service 就是一个 null 无法调用到最终的方法

多个Trait处理

在来看一种情况 如果你引入了2个Trait 而且其中都包含一个同名的方法 这个时候调用问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
trait A
{
public function showName()
{
echo 'reggie';
}
}
trait B
{
public function showName()
{
echo 'zyan';
}
}
class Test
{
use A,B;
public function show()
{
$this->showName();
}
}
$test = new Test();
$test->showName();
?>

这个时候如果你运行的话会有报错

1
2
3
4
vagrant@homestead:~/code$ php index.php
PHP Fatal error: Trait method showName has not been applied, because there are collisions with other trait methods on Test in /home/vagrant/code/index.php on line 18
Fatal error: Trait method showName has not been applied, because there are collisions with other trait methods on Test in /home/vagrant/code/index.php on line 18

解决冲突

使用 insteadof(代替)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Test
{
use A,B {
// 这里就声明了使用TraitA中的showName方法代替TraitB中的方法
A::showName insteadof B;
// 注意这个时候不能再去声明 B::showName insteadof A 这样的话又回到了原来了 有2个同名方法会报错 还是找不到你要调用哪个
}
public function show()
{
// 所以这里执行的话会输出 reggie
$this->showName();
}
}

使用 insteadof + as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Test
{
use A,B {
// 这里就声明了使用TraitA中的showName方法代替TraitB中的方法
A::showName insteadof B;
B::showName insteadof A;
A::showName as showNameA;
B::showName as showNameB;
}
public function show()
{
// 所以这里执行的话会输出 reggie
$this->showNameA();
}
}

关于代码拆分

其中看了 安正超 的文章后感觉学到了很多 引用下

Trait的优点在于随意组合,耦合性低,可读性高。

平常写代码的时候也许怎么拆分才是大家的痛点,分享以下几个技巧:

从需求或功能描述拆分,而不是写了两段代码发现代码一样就提到一起;
拆分时某些属性也一起带走,比如上面第一个例子里的价格,它是“可卖性”必备的属性;
拆分时如果给 Trait 起名困难时,请认真思考你是否真的拆分对了,因为正确的拆分是很容易描述 “它是一个具有什么功能的特性” 的;

参考

http://php.net/manual/zh/language.oop5.traits.php

https://overtrue.me/articles/2016/04/about-php-trait.html

https://zhuanlan.zhihu.com/p/31362082


-------------The End-------------