模型和迁移
有时候你希望你的包能够提供更多的内容。 假设我们在开发一个博客相关的包, 例如我们可能想要提供一个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()
函数做这些事情。该方法有两个参数:
-
一个包含文件路径的数组 ("源文件路径" => "目标路径")
-
第二个是我们分配给这组相关的可发布资源的名称(「标签」)。
实际上,我们可以按以下的例子实现此功能:
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
表这个测试仍然会报错。我们需要告诉我们的测试用例在运行测试前先执行所有的迁移。
让我们在 TestCase
的 getEnvironmentSetUp()
方法中加载迁移:
// '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();
});
然后运行这个测试,你就会看到它通过了。
为其他列添加测试
让我们为 body
和 author_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” 字段,就让我们在 Post
和 User
之间创建关联。但是...我们有一个问题,因为我们需要一个 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
模型,从而转移到功能测试。
验证 User
和 Post
之间的期望行为的单元测试可能如下所示:
// '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));
});
}
}
在此阶段,所有测试都应通过。
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
推荐文章: