前回のDB変更履歴保存のメカニズムは、Userのモデルのためだけですが、どのモデルでも同様なことが簡単にできるようにトレイトを作成してみます。

まず、audit_logsのテーブルを作成し直します。

どのモデルかわかるように項目を増やしました。前回、Userモデルのキーという意味でuser_idとしたところは、使用した会員のidの値が入るように変更しました。そして、モデルのキーは、model_keyという項目となっています。

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

class CreateAuditLogsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('audit_logs', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('route')->nullable(); // 実行したプログラムのルート
            $table->string('path')->nullable(); // 実行したプログラムのパス名
            $table->string('model')->nullable; // モデルのクラス名
            $table->unsignedBigInteger('model_key')->nullable(); // モデルのプライマリキー
            $table->unsignedBigInteger('user_id')->nullable(); // ユーザーのid
            $table->longText('old_value')->nullable(); // 変更前の値
            $table->longText('new_value')->nullable(); // 変更後の値
            $table->timestamps();
        });
    }

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

次に、トレイトを作成します。

前回、boot()の関数名が、bootAuditTrait()と、’boot’ + ‘AuditTrait’(トレイト名)となっていることに注意してください。


namespace App;

use Request;
use Route;

trait AuditTrait
{
	/**
	 * Bootイベント
	 *
	 * 関数名は、boot()でなく'boot' + トレイト名
	 *
	 * @return void
	 */
	public static function bootAuditTrait()
	{
	    // parent::boot()はコールしない

        static::created(function ($model) {
            $model->addAuditLog('created');
        });

        static::updated(function ($model) {
            $model->addAuditLog('updated');
        });

        static::deleted(function ($model) {
            $model->addAuditLog('deleted');
        });
	}

    public function addAuditLog($event)
    {
        $route = optional(Route::getCurrentRoute())->getName();
        $path = Request::path();
        $model = get_class($this);
        $model_key = $this->id;
        $user_id = optional(auth()->user())->id;

        $old_value = $new_value = null;

        if (in_array($event, ['created', 'updated'])) {
            $new_value = serialize($this->attributes);
        }

        if (in_array($event, ['updated', 'deleted'])) {
            $old_value = serialize($this->original);
        }

        AuditLog::create([
            'route'      => $route,
            'path'       => $path,
            'model'      => $model,
            'model_key'  => $model_key,
            'user_id'    => $user_id,
            'old_value'  => $old_value,
            'new_value'  => $new_value,
        ]);
    }
}

上のトレイトにより、Userのクラスでは、use AuditTraittの1行の追加だけで済みます。


namespace App;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Request;

class User extends Authenticatable
{
    use Notifiable;
    use AuditTrait;

    /**
     * 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',
    ];
}

さて、tinkerの出番です。変更をテストみましょう。

Psy Shell v0.10.4 (PHP 7.2.16 — cli) by Justin Hileman
>>> use App\AuditLog;

>>> $user = User::create(['name' => 'name1', 'email' => 'test1@example.com', 'password' => 'test']);
[!] Aliasing 'User' to 'App\User' for this Tinker session.
=> App\User {#3990
     name: "name1",
     email: "test1@example.com",
     updated_at: "2020-05-28 18:36:06",
     created_at: "2020-05-28 18:36:06",
     id: 1,
   }

>>> $user->update(['email' => 'test2@example.com']);
=> true

>>> $user->delete();
=> true

>>> AuditLog::all();
=> Illuminate\Database\Eloquent\Collection {#4015
     all: [
       App\AuditLog {#4016
         id: 1,
         route: null,
         path: "/",
         model: "App\User",
         model_key: 1,
         user_id: null,
         old_value: null,
         new_value: "a:6:{s:4:"name";s:5:"name1";s:5:"email";s:17:"test1@example.com";s:8:"password";s:4:"test";s:10:"updated_at";s:19:"2020-05-28 18:36:06";s:10:"created_at";s:19:"2020-05-28 18:36:06";s:2:"id";i:1;}",
         created_at: "2020-05-28 18:36:06",
         updated_at: "2020-05-28 18:36:06",
       },
       App\AuditLog {#4017
         id: 2,
         route: null,
         path: "/",
         model: "App\User",
         model_key: 1,
         user_id: null,
         old_value: "a:6:{s:4:"name";s:5:"name1";s:5:"email";s:17:"test1@example.com";s:8:"password";s:4:"test";s:10:"updated_at";s:19:"2020-05-28 18:36:06";s:10:"created_at";s:19:"2020-05-28 18:36:06";s:2:"id";i:1;}",
         new_value: "a:6:{s:4:"name";s:5:"name1";s:5:"email";s:17:"test2@example.com";s:8:"password";s:4:"test";s:10:"updated_at";s:19:"2020-05-28 18:36:10";s:10:"created_at";s:19:"2020-05-28 18:36:06";s:2:"id";i:1;}",
         created_at: "2020-05-28 18:36:10",
         updated_at: "2020-05-28 18:36:10",
       },
       App\AuditLog {#4018
         id: 3,
         route: null,
         path: "/",
         model: "App\User",
         model_key: 1,
         user_id: null,
         old_value: "a:6:{s:4:"name";s:5:"name1";s:5:"email";s:17:"test2@example.com";s:8:"password";s:4:"test";s:10:"updated_at";s:19:"2020-05-28 18:36:10";s:10:"created_at";s:19:"2020-05-28 18:36:06";s:2:"id";i:1;}",
         new_value: null,
         created_at: "2020-05-28 18:36:14",
         updated_at: "2020-05-28 18:36:14",
       },
     ],
   }
>>> 

前回と同様に、レコードの作成、編集、削除のすべてが、audit_logsのレコードに記録されています。

By khino