本书未发布

模型和迁移

未匹配的标注

有时候你希望你的包能够提供更多的内容。 假设我们在开发一个博客相关的包, 例如我们可能想要提供一个Post模型。这将要求我们处理模型、迁移、测试,甚至与Laravel附带的 App\User 模型连接关系。

模型

在我们包中的模型与我们在标准Laravel应用程序中使用的模型没有不同。 自从我们导入了 Orchestra Testbench , 我们就可以创建一个继承 Laravel Eloquent 模型的模型,并将它保存在 src/Models 目录。

// 'src/Models/Post.php'
<?php

namespace JohnDoe\BlogPackage\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
  // 禁用 Laravel 的批量赋值保护
  protected $guarded = [];
}

为了快速创建模型的迁移,我建议创建一个新的 Laravel 项目(仅用于创建模型、迁移等的「虚拟项目」)。接着使用 php artisan make:model -m 命令创建并复制模型到包的 src/Models 目录并使用恰当的命名空间。

迁移

迁移文件位于 Laravel 程序的 database/migrations 目录。在我们的包中,我们模仿此文件结构。因此,数据库迁移将不存在于 src / 目录中,而是存在于它们自己的 database/migrations 目录。 现在我们软件包的根目录至少包含两个文件夹: src/ 和 database/

生成迁移后,将其从「虚拟」Laravel 应用程序复制到包的 database/migrations 目录。 去掉文件名中的时间戳并且追加一个 .stub 的扩展名将它重命名为 create_posts_table.php.stub 。

// 'database/migrations/create_posts_table.php.stub'
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreatePostsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('posts');
    }
}

为了向最终用户展示我们的迁移, 我们需要注册包的 「发布事件」来发布迁移文件。 我们可以在包的服务提供者的 boot() 方法中使用publishes() 函数做这些事情。该方法有两个参数:

  1. 一个包含文件路径的数组 ("源文件路径" => "目标路径")

  2. 第二个是我们分配给这组相关的可发布资源的名称(「标签」)。

实际上,我们可以按以下的例子实现此功能:

class BlogPackageServiceProvider extends ServiceProvider
{
  public function boot()
  {
    if ($this->app->runningInConsole()) {
      // 发布的配置文件
      // 注册 `artisan` 命令

      if (! class_exists('CreatePostsTable')) {
        $this->publishes([
          __DIR__ . '/../database/migrations/create_posts_table.php.stub' => database_path('migrations/' . date('Y_m_d_His', time()) . '_create_posts_table.php'),
          // 你可以在这里添加更多的迁移
        ], 'migrations');
      }
    }
  }
}

在上面列出的代码中,我们首先检查应用程序是否在控制台中运行。接下来,我们将检查用户是否已经发布了迁移。如果没有,我们将在数据库路径的 migrations 文件夹中发布 create_posts_table 迁移,并以当前日期和时间为前缀。

现在可以通过以下方式在 「migrations」 标签下发布此软件包的迁移:

php artisan vendor:publish --provider="JohnDoe\BlogPackage\BlogPackageServiceProvider" --tag="migrations"

测试模型和迁移

在创建示例测试时,我们将在此处遵循测试驱动开发(TDD)的一些基础知识。 无论您是否在平时的工作流程中练习TDD,这些步骤都有助于揭示您在使用过程中可能遇到的问题,从而使您自己排除问题变得更加简单。让我们开始吧:

编写一个单元测试

现在假定我们已经设置好了 PHPunit , 首先在 tests/Unit 目录中创建一个名为  PostTest.php 单元测试来测试我们的 Post 模型。让我们写一个测试,验证 Post 存在一个 title :

// 'tests/Unit/PostTest.php'
<?php

namespace JohnDoe\BlogPackage\Tests\Unit;

use Illuminate\Foundation\Testing\RefreshDatabase;
use JohnDoe\BlogPackage\Tests\TestCase;
use JohnDoe\BlogPackage\Models\Post;

class PostTest extends TestCase
{
  use RefreshDatabase;

  /** @test */
  function a_post_has_a_title()
  {
    $post = factory(Post::class)->create(['title' => 'Fake Title']);
    $this->assertEquals('Fake Title', $post->title);
  }
}

注意:我们使用 RefreshDatabase trait 来确保我们在每次测试之前都从一个干净的数据库开始。

运行测试

我们可以通过 ./vendor/bin/phpunit 命令执行在 vendor 目录下的 phpunit 可执行文件来运行我们的测试用例。紧接着,我们可以通过在 composer.json 中添加一个「script」为这个测试起一个叫 test 的别名:

{
  ...,

  "autoload-dev": {},

  "scripts": {
    "test": "vendor/bin/phpunit",
    "test-f": "vendor/bin/phpunit --filter"
  }
}

现在外卖可以运行 composer test 来执行我们全部的测试用例, composer test-f 来运行指定的测试用例。

当我们运行 composer test-f a_post_has_a_title 的时候, 它将会显示如下错误:

InvalidArgumentException: Unable to locate factory with name [default] [JohnDoe\BlogPackage\Models\Post].

它告诉我们应该创建 Post 模型的模型工厂。

创建一个模型工厂

让我们在 database/factories 目录创建 PostFactory :

// 'database/factories/PostFactory.php'
<?php

use JohnDoe\BlogPackage\Models\Post;
use Faker\Generator as Faker;

$factory->define(Post::class, function (Faker $faker) {
  return [
    //
  ];
});

可是,因为我们没有在内存中的sqlite数据库中创建 posts 表这个测试仍然会报错。我们需要告诉我们的测试用例在运行测试前先执行所有的迁移。

让我们在 TestCasegetEnvironmentSetUp() 方法中加载迁移:

// 'tests/TestCase.php'

public function getEnvironmentSetUp($app)
{
  // 从迁移文件中导入 CreatePostsTable 类
  include_once __DIR__ . '/../database/migrations/create_posts_table.php.stub';

  // 运行迁移文件的 up() 方法
  (new \CreatePostsTable)->up();
}

现在,再次运行测试将导致预期的错误,即 posts 表上没有 title 列。让我们在create_posts_table.php.stub迁移中修复该问题:

// 'database/migrations/create_posts_table.php.stub'
Schema::create('posts', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->string('title');
    $table->timestamps();
});

然后运行这个测试,你就会看到它通过了。

为其他列添加测试

让我们为 bodyauthor_id 添加测试:

// 'tests/Unit/PostTest.php'
class PostTest extends TestCase
{
  use RefreshDatabase;

  /** @test */
  function a_post_has_a_title()
  {
    $post = factory(Post::class)->create(['title' => 'Fake Title']);
    $this->assertEquals('Fake Title', $post->title);
  }

  /** @test */
  function a_post_has_a_body()
  {
    $post = factory(Post::class)->create(['title' => 'Fake Body']);
    $this->assertEquals('Fake Title', $post->body);
  }

  /** @test */
  function a_post_has_an_author_id()
  {
    // 注意,在这里我们并不断言一个关系,仅仅是判断是否有一个列存储作者的 'id' 
    $post = factory(Post::class)->create(['author_id' => 999]); // 我们选择一个无穷大的author_id值,所以它不会和我们别的测试用例中的 author_id 冲突
    $this->assertEquals(999, $post->author_id);
  }
}

您可以继续使用TDD自行解决此问题,运行测试,而暴露下一个需要实现的测试,然后再次测试。

最终,您将完成模型工厂和迁移,如下所示:

// 'database/factories/PostFactory.php'
<?php

namespace JohnDoe\BlogPackage\Database\Factories;

use Faker\Generator as Faker;
use JohnDoe\BlogPackage\Models\Post;

$factory->define(Post::class, function (Faker $faker) {
    return [
        'title'     => $faker->words(3),
        'body'      => $faker->paragraph,
        'author_id' => 999,
    ];
});

到目前位置,尽管我们已经创建了 author_id 的测试,我们将会在下个章节中看到如何将用户模型建立关联关系。

// 'database/migrations/create_posts_table.php.stub'

Schema::create('posts', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->string('title');
    $table->text('body');
    $table->unsignedBigInteger('author_id');
    $table->timestamps();
});

将模型关联到 App\User

既然在 Post 模型上有一个 “author_id” 字段,就让我们在 PostUser 之间创建关联。但是...我们有一个问题,因为我们需要一个 User 模型,而该模型也是由全新安装的 Laravel 框架开箱即用提供的...

我们不能只提供自己的 User 模型,因为您可能希望最终用户能够将自己的 User 模型与您的 Post 模型挂钩。甚至更好的是,让最终用户决定要用哪个与 Post 模型关联。

使用多态关系

与其选择传统的一对多关系(一个用户可以有很多帖子,而一个帖子属于一个用户),我们将使用多态一对多关系,其中 Post 变相对应到某个相关模型(不一定是 User 模型)。

让我们比较一下标准关系和多态关系。

标准的一对多关系的定义:

// Post model
class Post extends Model
{
  public function author()
  {
    return $this->belongsTo(User::class);
  }
}

// User model
class User extends Model
{
  public function posts()
  {
    return $this->hasMany(Post::class);
  }
}

多态一对多关系的定义:

// Post model
class Post extends Model
{
  public function author()
  {
    return $this->morphTo();
  }
}

// User (or other) model
use JohnDoe\BlogPackage\Models\Post;

class Admin extends Model
{
  public function posts()
  {
    return $this->morphMany(Post::class, 'author');
  }
}

在将此 author() 方法添加到我们的 Post 模型之后,我们需要更新我们的 create_posts_table_migration.php.stub 文件以反映我们的多态关系。由于我们将方法命名为 “author”,因此 Laravel 需要一个 “author_id” 和 “ author_type” 字段,后者是所对应模型的命名空间字符串(例如 “App\User”)。

Schema::create('posts', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->string('title');
    $table->text('body');
    $table->unsignedBigInteger('author_id');
    $table->string('author_type');
    $table->timestamps();
});

现在,我们需要一种为最终用户提供选择的方式,以允许某些模型能够与我们的 Post 模型建立关系。 Traits 为这个确切的目的提供了一个很好的解决方案。

提供一个 Trait

src/ 目录中创建一个 Traits 文件夹,并添加以下 HasPosts trait:

// 'src/Traits/HasPosts.php'
<?php

namespace JohnDoe\BlogPackage\Traits;

use JohnDoe\BlogPackage\Models\Post;

trait HasPosts
{
  public function posts()
  {
    return $this->morphMany(Post::class, 'author');
  }
}

现在,最终用户可以在其任何模型(可能是 User 模型)中添加 use HasPosts 声明,该声明将自动在我们的 Post 模型中注册一对多关系。这样可以如下所示创建新的帖子:

// 假设我们有一个使用 HasPosts trait 的 User 模型
$user = User::first();

// 我们可以根据关系创建新帖子
$user->posts()->create([
  'title' => 'Some title',
  'body' => 'Some body',
]);

测试多态关系

当然,我们想证明使用我们的 HasPost trait 的任何模型确实可以创建新帖子,并且这些帖子被正确存储。

因此,我们将创建一个新的 User 模型,不是在 src/Models/ 目录中,而是在我们的 tests/ 目录中。

User 模型中,我们将使用标准 Laravel 项目附带的 User 模型可用的相同 trait,以保持接近真实场景。此外,我们使用自己的 HasPosts trait:

// 'tests/User.php'
<?php

namespace JohnDoe\BlogPackage\Tests;

use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\Access\Authorizable;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use JohnDoe\BlogPackage\Traits\HasPosts;

class User extends Model implements AuthorizableContract, AuthenticatableContract
{
    use HasPosts, Authorizable, Authenticatable;

    protected $guarded = [];

    protected $table = 'users';
}

现在我们有了一个 User 模型,我们还需要向我们的 database 目录中的 / migrations 中添加一个新的迁移 (Laravel 附带的标准用户表迁移),命名为 create_users_table.php.stub

// 'database/migrations/create_users_table.php.stub'
<?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->bigIncrements('id');
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }

    /**
     * 回滚迁移。
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users');
    }
}

通过在我们的 TestCase 中包含迁移并执行其 up() 方法,还可以在测试开始时加载迁移:

// 'tests/TestCase.php'
public function getEnvironmentSetUp($app)
{
    include_once __DIR__ . '/../database/migrations/create_posts_table.php.stub';
    include_once __DIR__ . '/../database/migrations/create_users_table.php.stub';

    // 运行up()方法(执行迁移)
    (new \CreatePostsTable)->up();
    (new \CreateUsersTable)->up();
}

更新我们的 Post 模型工厂

现在我们可以使用新工厂更新 User 模型,让我们在 PostFactory 中创建一个新的 User,然后将其分配给 “author_id” 和 “author_type”:

// 'database/factories/PostFactory.php'
<?php

namespace JohnDoe\BlogPackage\Database\Factories;

use Faker\Generator as Faker;
use JohnDoe\BlogPackage\Models\Post;
use JohnDoe\BlogPackage\Tests\User;

$factory->define(Post::class, function (Faker $faker) {
    $author = factory(User::class)->create();

    return [
        'title'         => $faker->words(3),
        'body'          => $faker->paragraph,
        'author_id'     => $author->id,
        'author_type'   => get_class($author),
    ];
});

接下来,我们更新 Post 单元测试,以验证是否可以指定 “author_type”。

// 'tests/Unit/PostTest.php'
class PostTest extends TestCase
{
  // 其他测试...

  /** @测试 */
  function a_post_has_an_author_type()
  {
    $post = factory(Post::class)->create(['author_type' => 'Fake\User']);
    $this->assertEquals('Fake\User', $post->author_type);
  }
}

最后,我们需要验证测试 User 是否可以创建 Post 并正确存储它。

由于我们不是在应用程序中使用对特定路由的调用来创建新帖子,因此我们也将该测试存储在 Post 单元测试中。在下一节“路由和控制器”中,我们将向端点发出 POST 请求,以创建新的 Post 模型,从而转移到功能测试。

验证 UserPost 之间的期望行为的单元测试可能如下所示:

// 'tests/Unit/PostTest.php'
class PostTest extends TestCase
{
  // 其他测试...

  /** @测试 */
  function a_post_belongs_to_an_author()
  {
    // 鉴于我们有一位作者
    $author = factory(User::class)->create();
    // 这个作者有一个帖子
    $author->posts()->create([
        'title' => 'My first fake post',
        'body'  => 'The body of this fake post',
    ]);

    $this->assertCount(1, Post::all());
    $this->assertCount(1, $author->posts);

    // 使用 tap() 将 $author->posts()->first() 别名为 $post
    // 提供更清晰和分组的断言
    tap($author->posts()->first(), function ($post) use ($author) {
        $this->assertEquals('My first fake post', $post->title);
        $this->assertEquals('The body of this fake post', $post->body);
        $this->assertTrue($post->author->is($author));
    });
  }
}

在此阶段,所有测试都应通过。

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://learnku.com/docs/laravel-package...

译文地址:https://learnku.com/docs/laravel-package...

上一篇 下一篇
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
贡献者:2
讨论数量: 0
发起讨论 只看当前版本


暂无话题~