経緯
先日実装したUndo/Redo機能(元に戻す/やり直す)がそれなりの工数がかかったので、
今後のためにざっくり残します。
スタッフのシフトを管理するWebアプリケーションに実装しました。
本記事ではバックエンド側の実装のみ残します。
※以前の関連記事
Vue.js3で「ctrl+z」「ctrl+shift+z」の処理を実装する
用語定義
- 履歴
- 画面操作した時のテーブルのレコードを保存したもの
- 元に戻す(Undo)
- 操作を過去に戻す↩️ 一つ過去の履歴を適用する
- やり直す(Redo)
- 操作を先に進める↪️ 一つ先の履歴を適用する
動作のイメージ
まず初めにブラウザ更新をして履歴1が作成され、何かしらの操作を3回行ったとする。
そうすると履歴2〜4が作成され、履歴4が適用されている状況になる。
仮に「元に戻す」を3回実行すると、履歴が過去へ戻り、履歴1が適用されている状況になる。
更にここで「やり直す」を1回実行すると、履歴が先に進み、履歴2が適用されている状況になる。
この状況で新たに操作を実行すると、履歴3以降が削除されて、新たな履歴(履歴3')が適用された状態になる。
※ブラウザが更新されたタイミングで、履歴が全て削除されて、履歴1から始まる。
Undo/Redo機能概要
履歴をリセットする
- 保存してある履歴を全て削除、テーブルからdeleteする
- ブラウザ更新された際に実行する
履歴を保存する
- その時の状態を履歴として保存、テーブルにinsertする
- 画面上で操作が発生した際に実行する
操作を元に戻す・やり直す
- 保存された履歴、テーブルからselectして、過去へ戻す or 先へ進める
- Undo/Redoボタン押下時に実行する
やり直す対象の履歴をリセットする
-
やり直す対象の履歴を全て削除、テーブルからdeleteする
- 過去の履歴は残す
- 画面上で操作が発生した際に実行する
テーブル構成
既存テーブル
- users
カラム | 型 | |
---|---|---|
id | int | PK |
name | varchar |
- staffs
カラム | 型 | |
---|---|---|
id | int | PK |
name | varchar |
- shifts
カラム | 型 | |
---|---|---|
id | int | PK |
date | datetime |
- shift_staffs
カラム | 型 | |
---|---|---|
id | int | PK |
shift_id | int | FK |
staff_id | int | FK |
start_time | int | |
end_time | int |
履歴テーブル ※今回実装
- histories
カラム | 型 | |
---|---|---|
id | int | PK |
user_id | int | FK |
status | int |
- history_shift_staffs
カラム | 型 | |
---|---|---|
id | int | PK |
history_id | int | FK |
shift_id | int | FK |
staff_id | int | FK |
start_time | int | |
end_time | int |
対象のテーブルが増える場合は、history_*
テーブルを作成して、history_id
をFKにする
※今回実際はhistory_shift_staffs
の子テーブルが存在していたので、その場合はhistory_id
ではなくhistory_shift_staff_id
をFKにしました
実装
具体的なコードは、いったんHistoryモデルとHistoryServiceクラスのみgithubに公開します。
※いずれ時間を取れたら整理したい…
https://github.com/shinji-yoshino/undo_redo_laravel
Historyモデル
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class History extends Model
{
use HasFactory;
const CURRENT = 1;
const UNDO_TARGET = 2;
const REDO_TARGET = 3;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'user_id',
'status',
];
public function history_shift_staffs(): HasMany
{
return $this->hasMany(HistoryShiftStaff::class);
}
}
HistoryServiceクラス
コード量が比較的多いので、ここではpublicメソッドの説明だけ書きます。
-
create_history()
- 履歴を作成するメソッドで、操作が発生するたびに呼び出される
-
copy_history_to_target()
- 履歴からシフト・画面に反映させる処理で、いわゆるUndo/Redo処理を行う
-
exist_undo_target()
- Undo対象の履歴が存在するか確認、Undoボタンの制御に使用
-
exist_redo_target()
- Redo対象の履歴が存在するか確認、Redoボタンの制御に使用