ricorocks-digital-agency / morpher
用于管理数据库迁移期间数据变更的 Laravel 扩展包。
Requires
- php: ^7.4|^8.0
- illuminate/support: ^8.0
Requires (Dev)
- orchestra/testbench: ^6.13
- pestphp/pest-plugin-expectations: ^1.0
- phpunit/phpunit: ^9.5
- spatie/laravel-ray: ^1.17
- spatie/macroable: ^1.0
README
我们都经历过。 你有一个正在生产的应用程序,现在其中一个数据库表需要结构变更。你不能手动进入并更改所有受影响的数据库行。那么你该怎么办?将逻辑放入迁移中?这似乎有点冒险,不是吗?
Morpher 是一个 Laravel 扩展包,它提供了一种统一模式,用于在数据库迁移之间转换数据。它允许你保持迁移逻辑简洁,并将数据操作的职责移动到更合适的位置。它还提供了一种强大的方式来编写这些转换的测试,否则这将是一项真正的挑战。
目录
安装
composer require ricorocks-digital-agency/morpher
这不是必需的,但你可能想发布配置文件
php artisan vendor:publish --tag=morpher
创建你的第一个 Morph
让我们设置一个示例场景。你的用户表有一个单独的 name
列,但现在你需要将其分开成 first_name
和 last_name
列。你的应用程序已经运行了一段时间,所以将需要进行数据转换。
你首先创建一个迁移
php artisan make:migration split_names_on_users_table
该迁移的 up
方法可能看起来像这样
public function up() { Schema::table('users', function (Blueprint $table) { $table->dropColumn('name'); $table->addColumn('first_name')->nullable(); $table->addColumn('last_name')->nullable(); }); }
那么,我们如何处理所有现有的名称,准备数据,并将其插入到我们的新表中呢?
让我们首先创建我们的第一个 Morph
php artisan make:morph SplitUserNames
这将创建一个名为 SplitUserNames
的新类在 database/morphs
中。我们的下一步是将我们的迁移链接到我们的新 Morph。我们可以使用 morph 类中的 $migration
属性来完成此操作
protected static $migration = SplitNamesOnUsersTable::class;
如果你需要更复杂的逻辑,你可以改用重写 migration
方法,并以此方式返回迁移类名。
⚡ 使用匿名迁移?你可以使用迁移的文件名作为
$migration
属性的值。例如:protected static $migration = "2021_05_01_000000_create_some_anonymous_table"
;
我们的下一个任务是描述我们的 Morph。在 app/Morphs/SplitUserNames
类中,我们需要做以下事情
- 检索迁移运行之前的当前名称。
- 将名称分成姓氏和名字。
- 在迁移完成后插入名称。
为此,我们的 Morph
可能如下所示
class SplitUserNames extends Morph { protected static $migration = SplitNamesOnUsersTable::class; protected $newNames; public function prepare() { // Get all of the names along with their ID $names = DB::table('users')->select(['id', 'name'])->get(); // Set a class property with the mapped version of the names $this->newNames = $names->map(function($data) { $nameParts = $this->splitName($data->name); return ['id' => $data->id, 'first_name' => $nameParts[0], 'last_name' => $nameParts[1]]; }); } protected function splitName($name) { // ...return some splitting logic here } public function run() { // Now we run the database query based on our transformed data DB::table('users')->upsert($this->newNames->toArray(), 'id'); } }
现在,当我们运行 php artisan migrate
时,这个 Morph 将会自动运行。
生命周期
理解 Morph
经历的生命周期可以帮助你充分利用它。
当 Morph
与迁移相关联,并且该迁移的 up
方法被运行(通常是从迁移数据库时),以下发生(按顺序)
- 在
Morph
类上调用prepare
方法。你可以在这里做任何需要准备数据的事情。 - 运行迁移。
- 在
Morph
类上调用canRun
方法。在这个方法中返回 false 将在这里停止进程。 - 在
Morph
类上调用run
方法。这是你应该执行数据转换的地方。
测试 Morph
数据变形带来的最大挑战之一是编写功能测试。在变形发生之前插入数据以进行测试变得非常棘手。然而,当您运行的代码将修改真实数据时,自动化测试非常重要。Morpher使得测试数据的过程变得轻松,您不再需要做出妥协。
要开始,我们建议为每个您想要编写测试的Morph创建一个单独的测试用例(或多个测试用例)。将TestsMorphs
特性添加到该测试类中,并将supportMorphs
调用添加到setUp
方法的末尾。
use RicorocksDigitalAgency\Morpher\Support\TestsMorphs; class UserMorphTest extends TestCase { use TestsMorphs; protected function setUp(): void { parent::setUp(); $this->supportMorphs(); } }
⚠️
TestsMorphs
特性与其他数据库特性(如RefreshDatabase
或DatabaseTransactions
)冲突。因此,请确保您的变形测试用例(在单独的测试类中)与其他测试套件中的测试隔离。
完成这些操作后,您就可以开始编写测试了!为此,我们提供了一个强大的检查API,以方便进行Morph测试。
use RicorocksDigitalAgency\Morpher\Facades\Morpher; class UserMorphTest extends TestCase { // ...After setup public function test_it_translates_the_user_names_correctly() { Morpher::test(UserMorph::class) ->beforeThisMigration(function($morph) { /** * We use the `beforeMigrating` hook to allow for "old" * data creation. In our user names example, we'll * create users with combined forename and surname. */ DB::table('users')->insert([['name' => 'Joe Bloggs'], ['name' => 'Luke Downing']]); }) ->before(function($morph) { /** * We use the `before` hook to perform any expectations * after the migration has run but before the Morph * has been executed. */ $this->assertCount(2, User::all()); }) ->after(function($morph) { /** * We use the `after` hook to perform any expectations * after the morph has finished running. For example, * we would expect data to have been transformed. */ [$joe, $luke] = User::all(); $this->assertEquals("Joe", $joe->forename); $this->assertEquals("Bloggs", $joe->surname); $this->assertEquals("Luke", $luke->forename); $this->assertEquals("Downing", $luke->surname); }); } }
如您所见,我们可以使用几种检查方法来全面测试我们的Morphs。请注意,您只需要使用与您的特定Morph相关的检查。
beforeThisMigration
此方法在数据库上运行的Morph
之前运行。它也在调用您的Morph上的prepare
方法之前运行。由于您的测试不会为Morph提供“旧”数据以进行更改,因此您可以使用此方法创建准备好的假数据,以便Morph使用。
请注意,在大多数情况下,您的Laravel Factories可能会过时,因此您可能需要求助于如
DB
Facade之类的手动方法。您还可以创建一个使用旧数据结构的版本化Factory。
before
此方法在调用您的Morph上的run
方法之前执行,但在执行prepare
方法之后。您可以使用此机会确保您的prepare方法已收集预期的数据并将其存储在Morph对象上,如果您的Morph需要执行此步骤。
after
此方法在调用run
方法之后执行。您应使用此方法检查数据迁移是否已成功运行以及您的数据是否确实已转换。
禁用 Morph
在本地开发中,特别是您经常销毁和重建数据库的情况下,禁用Morph运行可能会有所帮助。为此,请将以下内容添加到您的环境文件中
RUN_MORPHS=false
注意事项
run
方法中的所有内容都封装在数据库事务中。这意味着如果在运行您的变形时出现异常,则不会持久化任何数据更改。- 请记住,这个包并不是魔法。如果您对数据库中的数据进行了一些愚蠢的操作,就无法回退。**在迁移之前备份您的数据**。
- 您可以通过覆盖
canRun
方法来停止错误的数据集破坏您的数据库。在此方法中执行任何您想要的检查,只需返回一个布尔值来告诉我们是否应该继续。 - 想在变形期间将进度写入控制台?您可以使用Morph类上的
$this->console
属性来这样做!