
九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、幸運にも長らく開発のお仕事をさせていただいているのですが、その中には、「それほど需要が多いわけではないけど、たまに欲しい」と言われる機能があります。
それが・・・・・・
データ操作した人の情報を履歴として残しておく機能
です。
実際にご依頼をいただいたケースでは、「誰がそのデータを操作したのか」が分かるようになるので、使う人間に緊張感をもってもらうことができたり、もしくは珍しいケースでは「不正なデータ処理をしたらすぐバレるからやめとこ」となることが目的でした。(会社によって、いろんなケースがありますね)
そこで
今回はこの「操作した人の履歴保存」を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 ずん 飯尾さん」