如何使用和测试 Laravel 中的 Trait ?
PHP Traits 是在你的PHP类共享公共的功能奇妙的工具。起初,这看起来有点吓人。但是,我们很快就会有几个例子。
我们什么时候应该使用Traits?
在我们深入讨论这些例子之前,让我们花点时间讨论一下什么时候我们应该使用一个特质。很多时候,当我们添加一个包时,我们最终会使用一个Trait。利用包的特性是一种标准模式,因为它允许包编写器封装有用的功能,我们可以根据需要将这些功能添加到类中。
一个很好的例子是spatile laravel-permission 包. 通过将他们的HasRoles
Trait与我们的类联系起来,我们可以利用Trait功能管理角色
。
此外,在需要时Laravel框架本身使用Trait对层的功能和复杂性。著名的例子,你可能会跨运行包括AuthenticatesUsers
和Notifiable
。
最后,虽然利用别人提供的特质很好,但我们什么时候应该自己创造一个呢?通常,如果我们发现自己在许多类中重复了许多功能,那么最好制作一个。例如,如果我们的很多模型都使用UUID,那么我们可以生成一个Uuidable
特性。
一个不太常见的用例是使用Traits来分解一个 God class. 但在大多数情况下,有 better ways 做到这一点,除非你打算与许多其他类共享提取的功能,否则不推荐使用Traits。
现在我们可以通过一些例子来继续总结我们的特点。
Hello World 例子
让我们从最简单的Hello World 开始!我们从 PHP 文档中取个例子
<?php
Trait Hello {
public function sayHello() {
echo 'Hello ';
}
}
Trait World {
public function sayWorld() {
echo 'World';
}
}
class MyHelloWorld {
use Hello, World;
public function sayExclamationMark() {
echo '!';
}
}
$o = new MyHelloWorld();
$o->sayHello();
$o->sayWorld();
$o->sayExclamationMark();
?>
跟我们所期望的一样,将输出:
Hello World!
让我们再在拆解一下。第一步,我们像往常一下使用旧的php文档,然后创建Hello
Trait,定义一个方法叫sayHello()
, 主要是输出Hello
。
第二步,我们在定义一个 World
Trait,在里面定义一个SayWorld
方法,主要是输出World
。在方法下面,我们定义自己的类 MyHelloworld
。这让事情变的更有趣了。
你会注意到,我们会马上告诉我们的类使用我们定义的 Hello
跟 World
特性,这是十分重要的,因为这会让类知道它可以去使用的特性。在我们类里面,我们一个定义个方法叫sayExclamationMark()
,输出'!'
。
最后,我们是实例化我们的类并调用它使用特性如同使用自己的方法一样。它将跟我们期待的一样输出'Hello World!'
。
既然我们已经掌握了这个特性的窍门,我们可以看下如何去写跟测试一个真实的例子。
制作一个 UUID Trait
什么是 UUID?
通用唯一标识符(UUID)是一个128位的数字,我们可以使用它来标识如下所示的数据:
ceb580c4-8b8d-4c9c-85c9-5d3c39b6ed9c
通常,如果我们不想向公众公开数据的 id
,我们将使用UUID。例如,假设我们正在构建一个发票应用程序。我们不希望我们的用户能够在路径 /invoices/1
中看到他们的发票是系统中的第一张!知道你是这个应用程序的试验品,你会觉得有多安全?
这里还有一个安全问题。如果我们的路由是这样设置的,那么恶意用户通过系统中的所有发票进行增量操作将是微不足道的!当然,我们可以添加一些授权保护,但在数据级别上保持安全也不会有什么坏处。
既然我们知道了为什么要使用UUID,我敢打赌您可以看到,将一个UUID添加到我们的一堆模型中是一个很好的 Trait。所以,让我们潜进去做一个。
创建 Trait
我们将使用一个全新的Laravel项目来做测试,你也可以根据 installation guide 来安装和初始化该项目。
通过使用 ramsey/uuid这个优秀的扩展包来生成UUIDs, 命令运行 composer require ramsey/uuid
来安装该扩展包。
完成各项设置之后,在laravel项目的app
目录下创建一个Traits
的文件夹,并在Traits
目录下创建一个PHP类文件UuidTrait.php
。、
在UuidTrait.php
类文件中,添加命名空间、引用 ramsey/uuid
扩展包来定义我们需要的Trait。
<?php
namespace App\Traits;
use Ramsey\Uuid\Uuid;
trait UuidTrait
{
}
想一想我们自定义的Trait需要具有哪些功能?首先,我们需要定义一个key来作为我们要设置的UUID。其次,需要在模型创建的时候生成一个UUID。最后,还要提供一个方法,目的就是让模型来重写自身的boot()
方法。
我们先添加一个方法,返回代表模型UUID的字段。例如:uuid
。
trait UuidTrait
{
/**
* Defines the UUID field for the model.
* @return string
*/
protected static function uuidField()
{
return 'uuid';
}
}
这样,就会允许我们修改代表模型UUID的字段,有利于我们定义其他的方法。
如果,我们有一个模型不使用 uuid
而是使用 token
字段来作为唯一性的判断,没关系,这种情况下Trait照样能够轻松搞定。接下来,继续在Trait中添加一个boot()
方法。
trait UuidTrait
{
/**
* Defines the UUID field for the model.
* @return string
*/
protected static function uuidField()
{
return 'uuid';
}
/**
* Generate UUID v4 when creating model.
*/
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
$model->{self::uuidField()} = Uuid::uuid4()->toString();
});
}
}
很明显,这个 boot()
方法将重写所有使用该Trait的模型的boot()
方法。接下来我们只需要定义方法来来处理其他情况就可以了。 代码显示,我们添加了定义模型UUID的字段,以及模型创建时自动生成UUID的方法。
最后,只需要再添加一个方法来决定何时重写模型自身的boot()
方法。
trait UuidTrait
{
...
/**
* Use if boot() is overridden in the model.
*/
protected static function uuid()
{
static::creating(function ($model) {
$model->{self::uuidField()} = Uuid::uuid4()->toString();
});
}
}
如您所见,除了不覆盖默认的boot()
方法外,我们实现了boot()
相同的操作,同时避免了一些重复的操作。 最终的Trait如下所示:
<?php
namespace App\Traits;
use Ramsey\Uuid\Uuid;
trait UuidTrait
{
/**
* Generate UUID v4 when creating model.
*/
protected static function boot()
{
parent::boot();
self::uuid();
}
/**
* Defines the UUID field for the model.
* @return string
*/
protected static function uuidField()
{
return 'uuid';
}
/**
* Use if boot() is overridden in the model.
*/
protected static function uuid()
{
static::creating(function ($model) {
$model->{self::uuidField()} = Uuid::uuid4()->toString();
});
}
}
大功告成,现在我们拥有了一个完美的获取UUID的Trait。接下来在User
模型来使用一下吧,引入方法如下所示:
<?php
namespace App;
use App\Traits\UuidTrait;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use Notifiable, UuidTrait;
...
}
这就是我们需要在模型中做的。最后,我们可以在迁移文件中添加 uuid
:
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateUsersTable extends Migration
{
/**
* 运行迁移文件
*
* @return void
*/
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->uuid('uuid');
$table->rememberToken();
$table->timestamps();
});
}
...
}
现在我们可以在命令行运行 php artisan tinker
来检验是否有效。 我们运行 factory(\App\User::class)->create()
去创建一个带有 uuid
的用户。
现在我们得到它了! 由于我们的 trait,使我们的用户拥有 UUID。现在让我们使用 PHPUnit 增加一些适当的测试。
测试 Traits
在我们创建测试之前,我们需要进行一些设置。首先,我们需要将下面两行代码添加到 phpunit.xml
文件底部块中。
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
接下,运行 php artisan make:test UuidTraitTest --unit
来生成我们的测试。我们在这里使用了测试,因为测试一个 Trait 本质上和测试一个 model 是一样的。 我们的关注点在于验证低级,单一的方法是否按预期工作。所以,就选择了单元测试框。
有人可能会说,这样测试 Trait 似乎有点过头。我比较同意。覆盖 UUID 所用的功能测试更加合适并且更易于维护。但是,如果你想要单元测试测试所有的东西,谁来阻止你?
测试 Trait 有点麻烦,因为你不能单独实例化它。相反,你需要选择一个使用它的模型并测试它是否有效运行。
在我们的例子中,这很简单,因为我们仅仅使用到用户模型。但是,如果你是在自己的项目中,我建议你尽量挑选一些简单的模型去测试,以免使测试复杂化。现在,让我们来编写测试。
<?php
namespace Tests\Unit;
use App\User;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
class UuidTraitTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function users_have_a_uuid()
{
$user = factory(User::class)->create();
$this->assertTrue(isset($user->uuid));
}
}
这很简单。我们只是确保在生成用户的时候已设置 uuid
。现在,我们完成了测试!
总结
今天我们讨论了很多。我们学习了如何创建和使用 Trait。更重要的是,我们在思考 Trait 的使用场景以及为什么有用。在我们的编码不断进步的时候,学习如何做某事是很有价值的。但是,当我们能熟练使用我们所获得的经验和知识的时候,我们才能成为真正的专家。
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。