DB変更の履歴の作成をトレイトで簡単にEloquentのモデルに装着できるとしたころまでが前回ですが、巷では似たような仕組みでより良いパッケージがすでにあります。今回はそれを紹介します。
Laravel Auditing
パッケージは、Laravel Auditingという名でララベル専用です。
独自のサイトもあります。
http://www.laravel-auditing.com/
まずはインストール、
$ composer require owen-it/laravel-auditing
次に設定ファイルのパブリッシュ。
$ php artisan vendor:publish --provider "OwenIt\Auditing\AuditingServiceProvider" --tag="config"
config/audit.phpが作成されます。
DBテーブルの作成が必要なので、以下を実行してmigrationファイルを作成します。
$ php artisan vendor:publish --provider "OwenIt\Auditing\AuditingServiceProvider" --tag="migrations"
作成されたファイルを見てみましょう。
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateAuditsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('audits', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('user_type')->nullable();
            $table->unsignedBigInteger('user_id')->nullable();
            $table->string('event');
            $table->morphs('auditable');
            $table->text('old_values')->nullable();
            $table->text('new_values')->nullable();
            $table->text('url')->nullable();
            $table->ipAddress('ip_address')->nullable();
            $table->string('user_agent', 1023)->nullable();
            $table->string('tags')->nullable();
            $table->timestamps();
            $table->index(['user_id', 'user_type']);
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('audits');
    }
}
これを見ると、作成するDBテーブルの名前はauditsで、
user_type, user_idは、アクセスしたユーザーの情報、
old_valuesとnew_valuesはモデルの古い値と新規の値が入る、
url、ip_address, user_agentはウェブのどこからアクセスされたかの情報、などと検討できますが、その他の項目はどう使われるが興味あるところです。特に、morphs('auditable')の部分はなんでしょう?見たことないですね。
とりあえず、migrateしてテーブルを作成します。
$ php artisan migrate Migrating: 2020_06_05_152546_create_audits_table Migrated: 2020_06_05_152546_create_audits_table (0.04 seconds)
作成されたテーブルの構造を見ると、
mysql> describe audits; +----------------+---------------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +----------------+---------------------+------+-----+---------+----------------+ | id | bigint(20) unsigned | NO | PRI | NULL | auto_increment | | user_type | varchar(255) | YES | | NULL | | | user_id | bigint(20) unsigned | YES | MUL | NULL | | | event | varchar(255) | NO | | NULL | | | auditable_type | varchar(255) | NO | MUL | NULL | | | auditable_id | bigint(20) unsigned | NO | | NULL | | | old_values | text | YES | | NULL | | | new_values | text | YES | | NULL | | | url | text | YES | | NULL | | | ip_address | varchar(45) | YES | | NULL | | | user_agent | varchar(1023) | YES | | NULL | | | tags | varchar(255) | YES | | NULL | | | created_at | timestamp | YES | | NULL | | | updated_at | timestamp | YES | | NULL | | +----------------+---------------------+------+-----+---------+----------------+
 $table->morphs('auditable');は、auditable_typeとauditable_idの2つの項目が作成される結果となっていますが、何の値が入るのでしょうね。
モデルでの設定
さて、実際にモデルに設定してみましょう。私の仕組みと同様にトレイト(Auditable)を使用しますが、同じ名前のインターフェース(Auditable)も使われています。以下の「これが必要!」の部分です。
namespace App;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Request;
use OwenIt\Auditing\Contracts\Auditable; // これが必要!
class User extends Authenticatable implements Auditable // これが必要!
{
    use Notifiable;
    use \OwenIt\Auditing\Auditable; // これが必要!
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];
    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];
    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];
    /**
     * ここで監査したい項目を指定!
     *
     * @var array
     */
    protected $auditInclude = [
        'name',
        'email',
    ];
早速、tinkerで実行してみましょう。
tinkerはコンソールの実行なので、設定ファイルを以下のように変更する必要あります。
...
   /*
    |--------------------------------------------------------------------------
    | Audit Console
    |--------------------------------------------------------------------------
    |
    | Whether console events should be audited (eg. php artisan db:seed).
    |
    */
    'console' => true,
];
Psy Shell v0.10.4 (PHP 7.2.16 — cli) by Justin Hileman
>>> User::truncate(); // リセットして空にします。
>>> $user = User::create(['name' => 'name1', 'email' => 'test1@example.com', 'password' => 'test']);
[!] Aliasing 'User' to 'App\User' for this Tinker session.
=> App\User {#3995
     name: "name1",
     email: "test1@example.com",
     updated_at: "2020-06-05 17:01:38",
     created_at: "2020-06-05 17:01:38",
     id: 1,
   }
>>> $user->update(['email' => 'test2@example.com']);
=> true
>>> $user->audits; // 作成した監査レコードを取得
=> Illuminate\Database\Eloquent\Collection {#4005
     all: [
       OwenIt\Auditing\Models\Audit {#4021
         id: 1,
         user_type: null,
         user_id: null,
         event: "created",
         auditable_type: "App\User",
         auditable_id: 1,
         old_values: "[]",
         new_values: "{"name":"name1","email":"test1@example.com"}",
         url: "console",
         ip_address: "127.0.0.1",
         user_agent: "Symfony",
         tags: null,
         created_at: "2020-06-05 17:01:38",
         updated_at: "2020-06-05 17:01:38",
       },
       OwenIt\Auditing\Models\Audit {#4026
         id: 2,
         user_type: null,
         user_id: null,
         event: "updated",
         auditable_type: "App\User",
         auditable_id: 1,
         old_values: "{"email":"test1@example.com"}",
         new_values: "{"email":"test2@example.com"}",
         url: "console",
         ip_address: "127.0.0.1",
         user_agent: "Symfony",
         tags: null,
         created_at: "2020-06-05 17:01:43",
         updated_at: "2020-06-05 17:01:43",
       },
     ],
   }
なるほど、
         auditable_type: "App\User",
         auditable_id: 1,
これらの項目には、何のモデルのどのレコードの履歴という情報を残しているのですね。
引き続いて、レコード削除も試みると、
>>> $user->delete();
=> true
>>> DB::table('audits')->get();
=> Illuminate\Support\Collection {#3986
     all: [
       {#4019
         +"id": 1,
         +"user_type": null,
         +"user_id": null,
         +"event": "created",
         +"auditable_type": "App\User",
         +"auditable_id": 1,
         +"old_values": "[]",
         +"new_values": "{"name":"name1","email":"test1@example.com"}",
         +"url": "console",
         +"ip_address": "127.0.0.1",
         +"user_agent": "Symfony",
         +"tags": null,
         +"created_at": "2020-06-05 17:01:38",
         +"updated_at": "2020-06-05 17:01:38",
       },
       {#4025
         +"id": 2,
         +"user_type": null,
         +"user_id": null,
         +"event": "updated",
         +"auditable_type": "App\User",
         +"auditable_id": 1,
         +"old_values": "{"email":"test1@example.com"}",
         +"new_values": "{"email":"test2@example.com"}",
         +"url": "console",
         +"ip_address": "127.0.0.1",
         +"user_agent": "Symfony",
         +"tags": null,
         +"created_at": "2020-06-05 17:01:43",
         +"updated_at": "2020-06-05 17:01:43",
       },
       {#4032
         +"id": 3,
         +"user_type": null,
         +"user_id": null,
         +"event": "deleted",
         +"auditable_type": "App\User",
         +"auditable_id": 1,
         +"old_values": "{"name":"name1","email":"test2@example.com"}",
         +"new_values": "[]",
         +"url": "console",
         +"ip_address": "127.0.0.1",
         +"user_agent": "Symfony",
         +"tags": null,
         +"created_at": "2020-06-05 17:04:27",
         +"updated_at": "2020-06-05 17:04:27",
       },
     ],
   }
削除されたレコードゆえにUser::find()では取得できないので、クエリビルダーで取得しましたが、3番目のauditsのレコードが作成されていること確認できます。また、eventの項目に注目してもらうと、created, updated, deletedという値が入っていることも確認できます。
今回はここまでですが、他にもタグを追加できる機能や、古いレコードを自動的に削除する機能など将来紹介したい機能がたくさんあります。また、クラス定義のインターフェイス、つまり、implementsや、よく知らないmorphsのトピックも将来扱いたいです。
メルマガ購読の申し込みはこちらから。