anexia/laravel-changeset

一个用于监控模型变更并将它们记录到数据库中的Laravel模块

1.1.0 2020-09-15 07:52 UTC

This package is auto-updated.

Last update: 2024-09-17 21:10:54 UTC


README

一个用于检测您Eloquent模型(写入数据库)的永久更改并将它们记录到单独数据库表的Laravel包。

安装和配置

通过Composer安装模块,因此请修改您的composer.json中的require部分。

"require": {
    "anexia/laravel-changeset": "1.1.0"
}

在项目的config/app.php中添加新的服务提供者。这将包括包的迁移文件到项目的迁移路径。

return [
    'providers' => [        
        /*
         * Anexia Changeset Service Providers...
         */
        Anexia\Changeset\Providers\ChangesetServiceProvider::class,
    ]
];

现在运行

composer update [-o]

将包的源代码添加到您的/vendor目录并更新自动加载。

自定义数据库配置

默认情况下,更改集表会添加到应用程序的主数据库中。默认更改集数据库连接为'changeset_mysql',它在包的/config/connections.php中定义。

// from package's /config/connections.php:

    'changeset_mysql' => [
        'driver' => 'mysql',
        'host' => env('CHANGESET_DB_HOST', env('DB_HOST', '127.0.0.1')),
        'port' => env('CHANGESET_DB_PORT', env('DB_PORT', '3306')),
        'database' => env('CHANGESET_DB_DATABASE', env('DB_DATABASE', 'forge')),
        'username' => env('CHANGESET_DB_USERNAME', env('DB_USERNAME', 'forge')),
        'password' => env('CHANGESET_DB_PASSWORD', env('DB_PASSWORD', '')),
        'unix_socket' => env('CHANGESET_DB_SOCKET', env('DB_SOCKET', '')),
        'charset' => 'utf8mb4',
        'collation' => 'utf8mb4_unicode_ci',
        'prefix' => '',
        'strict' => true,
        'engine' => null,
    ],

因此更改集数据库默认使用.env中的'DB_'变量。要使用不同的更改集数据库,可以配置另一组.env变量。

# from .env:

CHANGESET_DB_CONNECTION=changeset_pgsql
CHANGESET_DB_HOST=127.0.0.1
CHANGESET_DB_PORT=5432
CHANGESET_DB_DATABASE=database
CHANGESET_DB_USERNAME=user
CHANGESET_DB_PASSWORD=pwd

/config/connections.php包含一系列预设的数据库配置(changeset_sqlite、changeset_mysql、changeset_pgsql、changeset_sqlsrv),类似于Laravel的默认数据库配置。在应用程序启动时,这些条目被添加到应用程序的'database.connections'配置字段。

如果需要不同的配置,可以在应用程序的/config/database.php中以通常的Laravel方式定义它,然后通过.env变量设置,例如:

// from application's /config/database.php
    'connections' => [
        ...
        
        'changeset_new' => [
            'driver' => 'mysql',
            'host' => env('CHANGESET_DB_HOST', env('DB_HOST', '127.0.0.1')),
            'port' => env('CHANGESET_DB_PORT', env('DB_PORT', '3306')),
            'db' => env('CHANGESET_DB_DATABASE', env('DB_DATABASE', 'forge')),
            'user' => env('CHANGESET_DB_USERNAME', env('DB_USERNAME', 'forge')),
            'pwd' => env('CHANGESET_DB_PASSWORD', env('DB_PASSWORD', '')),
            'socket' => env('CHANGESET_DB_SOCKET', env('DB_SOCKET', '')),
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix' => '',
            'strict' => true,
            'engine' => null,
        ],
    ],

# from .env: 

CHANGESET_DB_CONNECTION=changeset_new

工作原理

Changeset包包含三个新的模型

Changeset
    * id
    * action_id (uniquid() for each request/user action; related changesets share the same action_id; not nullable)
    * changeset_type (I = INSERT, U = UPDATE, D = DELETE; not nullable)
    * display (info text that describes the changes within the changeset; not nullable)
    * object_type_id (references to the name of the class that got changed within the changeset; foreign_key to 
        object_type; not nullable)
    * object_uuid (tracked identifier of the object; 'id' property by default; not nullable)
    * related_changeset_id (child changeset that triggered an additional changeset for a parent model; the parent model
        is defined by $trackRelated in the child class; foreign_key to changeset; nullable)
    * user_id (identifier of the user that triggered the change; must implement the ChangesetUserInterface; App\User by
        default; nullable)
    * created_at (timestamp of the changeset entry's creation in the database; auto-managed by Eloquent)
    * updated_at (timestamp of the changeset entry's last update in the database; auto-managed by Eloquent)
Changerecord
    * id
    * field_name (property that got changed; nullable)
    * changeset_id (One-To-Many relation to a changeset; one changeset can contain multiple changerecords; foreign_key
        to changeset; not nullable)
    * display (info text that describes the change on the field_name; not nullable)
    * is_deletion (boolean that marks a 'DELETE' event; 0 by default)
    * is_related (boolean that marks if the associated changeset has a related_changeset_id defined; 0 by default) 
    * new_value (value of the field_name after the change; nullable)
    * old_value (value of the field_name before the change; nullable)
    * created_at (timestamp of the changerecord entry's creation in the database; auto-managed by Eloquent)
    * updated_at (timestamp of the changerecord entry's last update in the database; auto-managed by Eloquent)
ObjectType
    * id
    * name (class that is trackable = uses ChangesetTrackable trait; unique; not nullable)
    * created_at (timestamp of the object type entry's creation in the database; auto-managed by Eloquent)
    * updated_at (timestamp of the object type entry's last update in the database; auto-managed by Eloquent)

Changeset包添加了对模型'created'、'updated'和'deleted'事件的钩子。因此,任何对可追踪模型的更改(save()、update()、delete()、forceDelete())都会触发更改集的创建。

根据执行的操作,会创建多个变更记录并将其关联到新的更改集中

  • 如果新模型被保存,所有其属性都会收到一个变更记录,并将其关联到其'CREATE'更改集(因为它们都已被'更改')。
  • 如果现有模型被更新,所有更改的属性(与更改前的值不同的值)都会收到一个变更记录,并将其关联到其'UPDATE'更改集。
  • 如果现有模型被删除,不会将任何变更记录与它的'delete'更改集相关联。

无论执行更改的类型如何,都会触发配置的父级关系(通过$trackRelated)的进一步更改集。父级关系本身可以定义自己的$trackRelated关联,因此可能触发多层相关更改集的树(请参阅“相关更改集”的使用)。

使用

在一个基模型类中使用ChangesetTrackable特质。所有需要被更改集追踪的模型都必须从这个基模型类扩展。

// base model class app/BaseModel.php
<?php

namespace App;

use Anexia\Changeset\Traits\ChangesetTrackable;
use Illuminate\Database\Eloquent\Model;

class BaseModel extends Model
{
    use ChangesetTrackable;
    
    // possible other "global" behaviour for all models can be put here
}


----------------------------------
// actual model class app/Post.php

<?php

namespace App;

class Post extends BaseModel
{
    protected $table = 'posts';
    protected $fillable = ['author_id', 'name', 'content'];
    protected $trackBy = 'id';
    protected $trackFields = ['author_id', 'name', 'content'];
    protected $tackRelated = ['author' => 'posts'];
    
    public function author()
    {
        return $this->belongsTo('App\User', 'author_id');
    }
}

----------------------------------
// actual model class app/User.php

<?php

namespace App;

class User extends BaseModel implements ChangesetUserInterface
{
    protected $table = 'users';
    protected $fillable = ['name', 'email'];
    protected $trackBy = 'id';
    protected $trackFields = ['name', 'email'];
    
    /**
     * Implementation of required interface method
     *
     * @return string
     */
    public function getUserName()
    {
        return $this->first_name . ' ' . $this->last_name;
    }
    
    /**
     * Implementation of required interface method
     *
     * Relation to Anexia\Changeset\Changeset model
     *
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function triggeredChangesets()
    {
        return $this->hasMany('Anexia\Changeset\Changeset', 'user_id');
    }
    
    public function posts()
    {
        return $this->hasMany('App\Post', 'author_id');
    }
}

通过一个“中间”BaseModel类进行绕行是必要的,以便允许ChangesetTrackable特质的受保护字段在每个可追踪模型中被重写。这意味着$trackBy、$trackFields和$trackRelated不能在BaseModel类本身中修改/重新定义,而是在每个子类(如Post)中修改。

对象类型

更改集包使用ObjectType模型来管理使用包的ChangesetTrackable特质的所有类。应用程序在其生命周期中“学习”并记住可追踪类的名称。在第一次发生每个使用ChanesetTrackable特质的类的追踪更改之后,类名将被存储在一个新的“ObjectType”条目中。

播种

为了一次性学习所有可能的对象类型(即使用 ChangesetTrackable 特性的类名),该包提供了一个 ChangesetObjectTypeSeeder。该种子会遍历 "app" 和 "vendor" 目录下的所有类,并检查它们(或它们的父类之一)是否使用了 ChangesetTrackable 特性。如果是的话,类的名称将被存储为新的对象类型。

运行种子器有两种选择

  1. 显式运行
php artisan db:seed --class="Anexia\\Changeset\\Database\\Seeds\\ChangesetObjectTypeSeeder"
  1. 将其包含在通用数据库种子器中,通常位于 /database/seeds/DatabaseSeeder.php
<?php

use Illuminate\Database\Seeder;
use Illuminate\Database\Eloquent\Model;

class DatabaseSeeder extends Seeder {

	public function run()
	{
		Model::unguard();

        // Changeset Seeds (ObjectType)
        $this->call(\Anexia\Changeset\Database\Seeds\ChangesetObjectTypeSeeder::class);
        $this->command->info('Changeset object types seeded!');

        // other seeds
		$this->call('AnotherSeeder');
		$this->command->info('Other data seeded!');
		
		...
    }
}

使用常见的种子命令

php artisan db:seed

对象类型种子器将与所有其他定义的种子器(按给定顺序)一起处理。

相关变更集

由于 Post.php 与另一个模型 User.php(通过 'author')有关系,并且已将其关系配置为 'trackable',因此对帖子对象的任何更改不仅会触发该帖子的变更集,还会触发其关联作者的变更集。所以

  • 如果保存了一个新的模型,则会创建一个带有对象类型 App\Post 的新 'CREATE' 变更集,所有属性都收到一个与该变更集关联的 changerecord。同时,还会创建一个带有对象类型 App\User 的新 'UPDATE' 变更集,其中包含字段名 'posts'、is_related = true 和显示信息,表明一个帖子对象已被新关联。

假设由管理员(即用户 1)为用户 2 创建了一个 id 为 1 的新帖子,将生成以下对象类型、变更集和变更记录

// object types
[
    {
        "id":1,
        "name":"App\Post"
    },
    {
        "id":2,
        "name":"App\User"
    }
]
// changeset and changerecords for post
{
    "id":1,
    "action_id":"59d5ced79454e",
    "changeset_type":"I",
    "display":"INSERT App\Post 1 at date 2017-10-05 10:02:46 by admin",
    "object_type_id":1,
    "object_uuid":1,
    "related_changeset_id":null,
    "user_id":1,
    "created_at":"2017-10-05 10:02:46",
    "updated_at":"2017-10-05 10:02:46",
    "changerecords":[
        {
            "id":1,
            "field_name":"author_id",
            "changeset_id":1,
            "display":"Set author_id to 1",
            "is_deletion":false,
            "is_related":false,
            "new_value":"1",
            "old_value":null,
            "created_at":"2017-10-05 10:02:46",
            "updated_at":"2017-10-05 10:02:46"
        },
        {
            "id":2,
            "field_name":"name",
            "changeset_id":1,
            "display":"Set name to First Post",
            "is_deletion":false,
            "is_related":false,
            "new_value":"First Post",
            "old_value":null,
            "created_at":"2017-10-05 10:02:46",
            "updated_at":"2017-10-05 10:02:46"
        },
        {
            "id":3,
            "field_name":"content",
            "changeset_id":1,
            "display":"Set content to This is my first post",
            "is_deletion":false,
            "is_related":false,
            "new_value":"This is my first post",
            "old_value":null,
            "created_at":"2017-10-05 10:02:46",
            "updated_at":"2017-10-05 10:02:46"
        }
    ]
}
// related changeset and changerecord for user (author)
{
    "id":2,
    "action_id":"59d5ced79454e",
    "changeset_type":"U",
    "display":"UPDATE App\User 1 at date 2017-10-05 10:02:46 after INSERT App\Post 1 by admin",
    "object_type_id":2,
    "object_uuid":1,
    "related_changeset_id":1,
    "user_id":1,
    "created_at":"2017-10-05 10:02:46",
    "updated_at":"2017-10-05 10:02:46",
    "changerecords":[
        {
            "id":4,
            "field_name":"posts",
            "changeset_id":2,
            "display":"Changed posts associations to [{"id":1}]",
            "is_deletion":false,
            "is_related":true,
            "new_value":"[{"id":1}]",
            "old_value":null,
            "created_at":"2017-10-05 10:02:46",
            "updated_at":"2017-10-05 10:02:46"
        }
    ]
}
  • 如果现有的帖子被更新,则会创建一个新的带有对象类型 App\Post 的 'UPDATE' 变更集,所有更改的属性都会收到一个与该变更集关联的 changerecord。同时,还会创建一个新的带有对象类型 App\User 的 'UPDATE' 变更集,其中包含字段名 'posts'、is_related = true 和显示信息,表明某个关联的帖子已被更改。假设现有的帖子(id 为 1)被管理员(即用户 1)更改,将生成以下变更集和变更记录
// changeset and changerecords for post
{
    "id":3,
    "action_id":"59d5f7c928e33",
    "changeset_type":"U",
    "display":"UPDATE App\Post 1 at date 2017-10-05 12:03:10 by admin",
    "object_type_id":1,
    "object_uuid":1,
    "related_changeset_id":null,
    "user_id":1,
    "created_at":"2017-10-05 12:03:10",
    "updated_at":"2017-10-05 12:03:10",
    "changerecords":[
        {
            "id":1,
            "field_name":"author_id",
            "changeset_id":3,
            "display":"Set author_id to 1",
            "is_deletion":false,
            "is_related":false,
            "new_value":"1",
            "old_value":null,
            "created_at":"2017-10-05 12:03:10",
            "updated_at":"2017-10-05 12:03:10"
        }
    ]
}
// related changeset and changerecord for user (author)
{
    "id":4,
    "action_id":"59d5f7c928e33",
    "changeset_type":"U",
    "display":"UPDATE App\User 1 at date 2017-10-05 12:03:10 after UPDATE App\Post 1 by admin",
    "object_type_id":2,
    "object_uuid":1,
    "related_changeset_id":3,
    "user_id":1,
    "created_at":"2017-10-05 12:03:10",
    "updated_at":"2017-10-05 12:03:10",
    "changerecords":[
        {
            "id":4,
            "field_name":"posts",
            "changeset_id":4,
            "display":"Associated posts still are [{"id":1}]",
            "is_deletion":false,
            "is_related":true,
            "new_value":"[{"id":1}]",
            "old_value":null,
            "created_at":"2017-10-05 12:03:10",
            "updated_at":"2017-10-05 12:03:10"
        }
    ]
}
  • 如果现有的帖子被删除,则会创建一个新的带有对象类型 App\Post 的 'DELETE' 变更集。同时,还会创建一个新的带有对象类型 App\User 的 'UPDATE' 变更集,其中包含字段名 'posts'、is_related = true、is_deletion = true 和显示信息,表明某个关联的帖子已被删除。
// changeset (with no changerecords) for post
{
    "id":5,
    "action_id":"59d5fd3f34bf9",
    "changeset_type":"D",
    "display":"DELETE App\Post 1 at date 2017-10-05 12:05:51 by admin",
    "object_type_id":1,
    "object_uuid":1,
    "related_changeset_id":null,
    "user_id":1,
    "created_at":"2017-10-05 12:05:51",
    "updated_at":"2017-10-05 12:05:51"
}
// related changeset and changerecord for user (author)
{
    "id":6,
    "action_id":"59d5fd3f34bf9",
    "changeset_type":"U",
    "display":"UPDATE App\User 1 at date 2017-10-05 12:05:51 after DELETE App\Post 1 by admin",
    "object_type_id":2,
    "object_uuid":1,
    "related_changeset_id":5,
    "user_id":1,
    "created_at":"2017-10-05 12:05:51",
    "updated_at":"2017-10-05 12:05:51",
    "changerecords":[
        {
            "id":5,
            "field_name":"posts",
            "changeset_id":6,
            "display":"Deleted posts associations",
            "is_deletion":true,
            "is_related":true,
            "new_value":"[]",
            "old_value":null,
            "created_at":"2017-10-05 12:03:10",
            "updated_at":"2017-10-05 12:03:10"
        }
    ]
}

开发者列表

项目相关外部资源