如何使用和测试 Laravel 中的 Trait ?

Laravel

PHP Traits 是在你的PHP类共享公共的功能奇妙的工具。起初,这看起来有点吓人。但是,我们很快就会有几个例子。

我们什么时候应该使用Traits?

在我们深入讨论这些例子之前,让我们花点时间讨论一下什么时候我们应该使用一个特质。很多时候,当我们添加一个包时,我们最终会使用一个Trait。利用包的特性是一种标准模式,因为它允许包编写器封装有用的功能,我们可以根据需要将这些功能添加到类中。

一个很好的例子是spatile laravel-permission 包. 通过将他们的HasRolesTrait与我们的类联系起来,我们可以利用Trait功能管理角色

此外,在需要时Laravel框架本身使用Trait对层的功能和复杂性。著名的例子,你可能会跨运行包括AuthenticatesUsersNotifiable

最后,虽然利用别人提供的特质很好,但我们什么时候应该自己创造一个呢?通常,如果我们发现自己在许多类中重复了许多功能,那么最好制作一个。例如,如果我们的很多模型都使用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

第二步,我们在定义一个 WorldTrait,在里面定义一个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 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://nick-basile.com/blog/post/how-to...

译文地址:https://learnku.com/laravel/t/36243

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!