九保すこひ@フリーランスエンジニア|累計300万PVのブログ運営中
さてさて、幸運にも長らく開発のお仕事をさせていただいているのですが、その中には、「それほど需要が多いわけではないけど、たまに欲しい」と言われる機能があります。
それが・・・・・・
データ操作した人の情報を履歴として残しておく機能
です。
実際にご依頼をいただいたケースでは、「誰がそのデータを操作したのか」が分かるようになるので、使う人間に緊張感をもってもらうことができたり、もしくは珍しいケースでは「不正なデータ処理をしたらすぐバレるからやめとこ」となることが目的でした。(会社によって、いろんなケースがありますね)
そこで❗
今回はこの「操作した人の履歴保存」をLaravel
で実装してみます。
ぜひ皆さんのお役にたてると嬉しいです😊✨
(最後の実際に開発したソースコードをダウンロードできますよ👍)
「この間教えてもらった obniz に
興味津々です❗」
開発環境: Laravel 8.x
やりたいことの詳細
特定のテーブルに以下4つの処理があったら履歴を残すようにします。
- データ追加
- データ変更
- データ削除
- データ閲覧
そして、保存する履歴データの中身は以下3つです。
- いつ: 操作が行われた時間
- 誰が: ユーザーID
- 何を: どこのテーブルのどのデータを操作したか
なお、せっかく作るのでどんなテーブルでも汎用的に使えるように実装していきます。
では、楽しんでやっていきましょう❗
前提として
すでにLaravel
にログイン機能がインストールされ、さらにテストユーザーが登録済みであることが前提です。
もしまだの方は以下のURLを参考にして先に作業を済ませておいてください。
📝 参考URL
履歴を保存するテーブルを作成する
では、まずは「いつ/誰が/何を操作したか」がわかる履歴を管理するテーブルを作成します。
以下のコマンドを実行してください。
php artisan make:model ModelHistory -m
すると、マイグレーションとモデルが一気に作成されますので、マイグレーションを以下のように変更してください。
// 省略 public function up() { Schema::create('model_histories', function (Blueprint $table) { $table->id(); $table->string('model')->comment('モデル名'); $table->unsignedBigInteger('model_id')->comment('モデルのID'); $table->unsignedBigInteger('user_id')->comment('操作したユーザー'); $table->string('operation_type')->comment('操作のタイプ'); // created, updated, deleted, retrieved $table->timestamps(); $table->foreign('user_id')->references('id')->on('users'); }); } // 省略
マイグレーションの変更ができたら以下のコマンドで実際にテーブルを作成してください。
php artisan migrate
完了するとテーブルはこうなります。
Traitをつくる
では、履歴を自動保存する部分をつくっていきますが、使い回しがしやすいようにTrait
で実行するようにします。
以下のファイルを作成してください。
app/Traits/ModelHistoryTrait.php
<?php namespace App\Traits; use App\Models\ModelHistory; trait ModelHistoryTrait { public static function saveModelHistory($types = ['created', 'updated', 'deleted', 'retrieved']) { if(auth()->check()) { foreach($types as $type) { forward_static_call([__CLASS__, $type], function($model) use($type){ $user = auth()->user(); $history = new ModelHistory(); $history->model = get_class($model); $history->model_id = $model->id; $history->user_id = $user->id; $history->operation_type = $type; $history->save(); }); } } } // Relationship public function model_histories() { return $this->hasMany(ModelHistory::class, 'model_id', 'id') ->where('model', __CLASS__); } }
少し見慣れない関数forward_static_call
が入っていますが、この中でやっているのは、以下4つのイベントを用意しているだけです。
static::created(function($model){ // }); static::updated(function($model){ // }); static::deleted(function($model){ // }); static::retrieved(function($model){ // });
つまり、ループを使って動的に実行するようにし、省コード化しています。(ただ、おそらく私もforward_static_call()
を使ったのは初めてです。さすがPHP。関数が豊ですね😂)
そして、model_histories()
は、該当データに関連する全履歴データをリレーションシップで取得できるようにしています。
ここで注目してほしいのが、where('model', __CLASS__)
の部分です。これをつけていないと、別モデルの同じIDを持つデータまで取得してしまうことになるからです。そのため、クラス名を使って絞り込みをしています。
テスト用モデルをつくる
では、この時点ですでに履歴保存用のコードは完了していますが、テスト用にSong
、そしてBook
というモデルと、それらに対応するテーブルを作っておきましょう。
※ なお、ほぼ同じ内容ですので、今回はSong
のみのご紹介をします。
以下のコマンドを実行してください。
php artisan make:model Song -m
すると、モデルとマイグレーションが作成されるので、まずモデルの中身を以下のようにしてください。
app/Models/Song.php
<?php namespace App\Models; use App\Traits\ModelHistoryTrait; // 👈 ここを追加しました use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Song extends Model { use HasFactory, ModelHistoryTrait; // 👈 ここを追加しました public static function boot() // 👈 ここを追加しました { parent::boot(); self::saveModelHistory(); } }
続いてマイグレーションです。(追加するフィールドはお好みで自由に変更してもOKです👍)
database/migrations/****_**_**_******_create_songs_table.php
// 省略 public function up() { Schema::create('songs', function (Blueprint $table) { $table->id(); $table->string('name'); $table->timestamps(); }); } // 省略
では、Book
も同様に作業をしてから以下のコマンドを実行してください。
php artisan migrate
実行が完了するとテーブルはそれぞれ以下のようになります。
テストする
では、実際にテストしてみましょう❗
実行手順は以下のとおりです。
- ログイン
- データ追加
- データ変更
- データ閲覧
- データ削除
ルートに以下のコードを追加してください。
routes/web.php
Route::get('model_history', function(){ // ログイン auth()->loginUsingId(1); // データ追加 $song = new \App\Models\Song(); $song->name = 'テスト曲名1'; $song->save(); sleep(1); // データ変更 $song->name = 'テスト曲名2'; $song->save(); sleep(1); // データ閲覧 $song = \App\Models\Song::first(); sleep(1); // データ削除 $song->delete(); });
内容としては、「IDが1
のユーザーで、songs
テーブルに対し各4つの操作を行う」です。
※ なお、履歴データの時間が同じにならないように各操作の間にsleep()
をはさんでいます。
では「http://******/model_history」にアクセスしてしてみましょう。
すると・・・・・・
はい❗
うまくテーブルmodel_histories
に各データが登録が登録されています。
では続いて、ルートの中身を「IDが2
のユーザーで、今度はbooks
テーブルに対して各4つの操作する」に変更して再度実行してみましょう。
routes/web.php
Route::get('model_history', function(){ // ログイン auth()->loginUsingId(2); // データ追加 $book = new \App\Models\Book(); $book->name = 'テスト本の名前1'; $book->save(); sleep(1); // データ変更 $book->name = 'テスト本の名前2'; $book->save(); sleep(1); // データ閲覧 $book = \App\Models\Book::first(); sleep(1); // データ削除 $book->delete(); });
すると・・・・・・
別のモデルでも履歴を登録することができました。
成功です😊✨
では、せっかくですのでモデルに追加したリレーションシップもチェックしておきましょう。
まず、データが物理削除されてしまうとリレーションシップどころかデータ取得ができなくなってしまいますので、ソフトデリートを有効にしておきます。
まず、マイグレーションにsoftDeletes()
を追加してください。
database/migrations/****_**_**_******_create_songs_table.php
Schema::create('songs', function (Blueprint $table) { $table->id(); $table->string('name'); $table->softDeletes(); // 👈 ここを追加しました $table->timestamps(); });
次に、以下のコマンドでテーブルを再構築します。
php artisan migrate:fresh --seed
するとテーブルは以下のように変更になりました。
続いて、Song
モデルがソフトデリートに対応するようにしておきます。
<?php namespace App\Models; use App\Traits\ModelHistoryTrait; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; // 👈 ここを追加しました class Song extends Model { use HasFactory, ModelHistoryTrait, SoftDeletes; // 👈 ここを追加しました // 省略 }
では、この状態で以下のコードを実行してみましょう。
routes/web.php
Route::get('model_history', function(){ // ログイン auth()->loginUsingId(1); // データ追加 $song = new \App\Models\Song(); $song->name = 'テスト曲名1'; $song->save(); sleep(1); // データ変更 $song->name = 'テスト曲名2'; $song->save(); sleep(1); // データ削除(ソフトデリート) $song->delete(); sleep(1); // データ閲覧 $song = \App\Models\Song::withTrashed()->with('model_histories')->first(); dd($song->toArray()); });
なお、この中で重要なのがwithTrashed()
です。
これをつけていると、すでにソフトデリートされたデータも取得することができるようになります。
そして、さらにwith('model_histories')
でリレーションシップで履歴データを呼び出しています。
では、実行してみます。
すると・・・・・・
はい❗
関連する履歴データがmodel_histories
として取得できています。
成功です😊👍✨
ちなみに
先ほどつくったModelHistoryTrait
のsaveModelHistory()
は、引数で「どの履歴を保存するか」を指定することができます。
例えば、deleted
だけ履歴を残したい場合は以下のようになります。
app/Models/Song.php
<?php // 省略 class Song extends Model { use HasFactory, ModelHistoryTrait, SoftDeletes; public static function boot() { parent::boot(); self::saveModelHistory(['deleted']); // 👈 ここです } }
ダウンロードする
今回実際に開発したソースコード一式を以下からダウンロードすることができます。
追加・変更・削除・閲覧した人の履歴を自動で保存する※ ただし、マイグレーション等はご自身で行っていただく必要があります。
おわりに
ということで、今回はデータの操作履歴を残すための機能を作ってみました。
Laravel
にはイベントなど「ラク」できる機能がたくさんついていますので、予想以上にシンプルなコードで完成させることができました👍
なお、この機能を拡張するアイデアとしては、どのデータからどのデータに変更されたかというold_value
, new_value
みたいなフィールドをつくっておけば、あとでより詳しい履歴データを確認することができるかと思います。
ぜひみなさんも、いろいろとアイデアを考えてみてくださいね。
ではでは〜❗
「あぁ〜、ジェフ・ベゾスがお兄ちゃんだったらな〜
ゴロゴロ〜(笑) by ずん 飯尾さん」