
九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、この間このブログで以下の記事を公開しました。
参考ページ: Laravel Breeze: Multi Auth しながらログイン・登録・パスワード再発行・Email Verification に対応させる
そして、このようなMulti-Auth
(専用のユーザーテーブルを利用したログイン)を使ったときに「あ、これちょっと難しいかも」と思った機能がありました。
それは・・・・・・
メッセージ機能
です。
もちろん、ログインするのがusers
だけならsender_id
, receiver_id
をカラムに持たせて保存すればシンプルでいいのですが、Multi-Auth
の場合はそうはいきませんよね。
そこで
今回はMulti-Auth
してるLaravel
で以下のように別ユーザーテーブルを使ったメッセージ機能を作ってみることにしました。
- 生徒 –> 先生 へメッセージ送信
- 先生 –> 生徒 〃
- 生徒 –> 別の生徒 〃
- 先生 –> 別の先生 〃
ぜひ何かの参考になりましたら嬉しいです。
令和の虎
『バカは成功に学んで失敗し、
利口は失敗に学んで成功する』
耳が痛いですな…
開発環境: Laravel 10.x
目次 [非表示]
前提として
Laravel Breeze
を使ったMulti-Auth
に対応できていること(専用テーブルでログインができること)が前提です。
そして、今回は必要なのは以下2つのユーザーを使ったログイン機能です。
- 生徒: Student
- 先生: Teacher
※ ホントはParent
も入れたかったですが、複雑になるので今回は2つにしました。
もしまだの方は以下のページを参考にしてみてください。
参考ページ: Laravel Breeze: Multi Auth しながらログイン・登録・パスワード再発行・Email Verification に対応させる
やりたいこと
そして、先ほど挙げた2つのユーザー間で以下のようなメッセージ送信ができるようにします。
- 「Student」から「Teacher」へメッセージ送信
- 「Teacher」から「Student」へメッセージ送信
- 「Student」から「Student」へメッセージ送信
- 「Teacher」から「Teacher」へメッセージ送信
つまり、自分以外のユーザーなら「生徒」「先生」関係なく誰にでもメッセージが送れるようします。
事前知識として
私も複雑なDB構成はそれほど得意じゃないですが、今回は「ポリモーフィック関連」と呼ばれるアンチパターンを避けた構成にしています。
ポリモーフィック関連とは、シンプルに言うとリレーションシップが複数のテーブルを参照する構造のことで、例えば、
- このコメントは「teachers」の ID: 3 の人が書いたもの
- こっちのコメントは「students」の ID: 5 の人が書いたもの
というようにリレーションシップ先が可変になっています。
しかし、これをすると外部キーを設定することができないためアンチパターンとされているようです。(個人的にはLaravel
のイベントをつけておけば大丈夫なんじゃないかなとも思うのですが、これも絶対ではありません)
実際のテーブルで言うと以下のようになります。
では、(今回は使いませんが)これも踏まえつつ、楽しんでやっていきましょう
DBまわりのファイルをつくる
では、モデル&マイグレーション、Seeder
などのDB
関連の部分からはじめていきましょう。
以下4つのコマンドを実行してください。
php artisan make:model MultiAuthUser -m
php artisan make:model Student -ms
php artisan make:model Teacher -ms
php artisan make:model Message -m
これで、全10ファイルが作成されることになりますので、中身をそれぞれ変更していきましょう。
※ なお、構造としては、MultiAuthUser
がStudent
とTeacher
の親テーブルになり、Message
に登録するのは全てMultiAuthUser
のIDということになります。
モデルの中身
では、まずはモデルです。
app/Models/Student.php
<?php
namespace App\Models;
use App\Traits\MessageTrait;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
class Student extends Authenticatable
{
use HasFactory, MessageTrait;
// Relationship
public function multi_auth_user()
{
return $this->belongsTo(MultiAuthUser::class, 'multi_auth_user_id', 'id');
}
}
app/Models/Teacher.php
<?php
namespace App\Models;
use App\Traits\MessageTrait;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
class Teacher extends Authenticatable
{
use HasFactory, MessageTrait;
// Relationship
public function multi_auth_user()
{
return $this->belongsTo(MultiAuthUser::class, 'multi_auth_user_id', 'id');
}
}
Trait で共通部分をつくる
先ほどのStudent
とTeacher
モデルにはどちらにもメッセージ送信ができるsendMessage()
が必要です。
ただ、シンプルに両方に同じコードを書いてしまうと後で変更があったとき全てを変更しないといけなくなるので、メンテンナンスがしにくくなります。
そこで、今回はTrait
で共通化し、コードが1か所だけで済むようにしておきましょう。
app/Traits/MessageTrait.php
<?php
namespace App\Traits;
use App\Models\Message;
trait MessageTrait
{
public function sendMessage($message_body, $receiver)
{
$message = new Message();
$message->sender_id = $this->multi_auth_user->id;
$message->receiver_id = $receiver->multi_auth_user->id;
$message->body = $message_body;
$message->save();
return $message;
}
}
なお、このトレイトはすでにStudent
とTeacher
モデルにセット済です。
Enum でユーザータイプを管理しやすくする
これも先ほどと同じく「共通化(データの一元管理)」するテクニックなのですが、今回使う以下2つの「ユーザータイプ」を管理します。
- 生徒: Student
- 先生: Teacher
実際には以下のようになります。
app/Enums/MultiAuthUserEnum.php
<?php
namespace App\Enums;
use Illuminate\Support\Arr;
enum MultiAuthUserEnum: string
{
case Student = 'student';
case Teacher = 'teacher';
public function label()
{
return match ($this) {
self::Student => '生徒',
self::Teacher => '先生',
};
}
public static function values()
{
$cases = self::cases();
return Arr::map($cases, fn($case) => $case->value);
}
}
※ ちなみにEnum
はPHP 8.1
から使えるようになった機能です。
マイグレーションの中身
では、データベース・テーブルの設計図とも言えるマイグレーションを作っていきます。
database/migrations/****_**_**_******_create_multi_auth_users_table.php
// 省略
public function up(): void
{
Schema::create('multi_auth_users', function (Blueprint $table) {
$table->id();
$table->timestamps();
});
}
// 省略
database/migrations/****_**_**_******_create_students_table.php
// 省略
public function up(): void
{
Schema::create('students', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('multi_auth_user_id')->comment('Multi-AuthユーザーID');
$table->string('email')->unique()->comment('メールアドレス');
$table->string('password')->comment('パスワード');
$table->string('name')->comment('名前');
$table->timestamps();
$table->foreign('multi_auth_user_id')->references('id')->on('multi_auth_users')->onDelete('cascade');
});
}
// 省略
database/migrations/****_**_**_******_create_teachers_table.php
// 省略
public function up(): void
{
Schema::create('teachers', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('multi_auth_user_id')->comment('Multi-AuthユーザーID');
$table->string('email')->unique()->comment('メールアドレス');
$table->string('password')->comment('パスワード');
$table->string('name')->comment('名前');
$table->timestamps();
$table->foreign('multi_auth_user_id')->references('id')->on('multi_auth_users')->onDelete('cascade');
});
}
// 省略
database/migrations/****_**_**_******_create_messages_table.php
// 省略
public function up(): void
{
Schema::create('messages', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('sender_id')->comment('送信者ID');
$table->unsignedBigInteger('receiver_id')->comment('受信者ID');
$table->text('body')->comment('メッセージ内容');
$table->timestamps();
$table->foreign('sender_id')->references('id')->on('multi_auth_users')->onDelete('cascade');
$table->foreign('receiver_id')->references('id')->on('multi_auth_users')->onDelete('cascade');
});
}
// 省略
Seeder の中身
では、自動でStudent
とTeacher
の「テストデータ」が作れるようにSeeder
を作っていきましょう。
database/seeders/StudentSeeder.php
<?php
namespace Database\Seeders;
use App\Models\MultiAuthUser;
use App\Models\Student;
use Database\Factories\StudentFactory;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
class StudentSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
for($i = 1; $i <= 10; $i++) {
// Multi-Auth
$multi_auth_user = new MultiAuthUser();
$multi_auth_user->save();
// Student
$email = 'student' . $i . '@example.com';
$student = new Student([
'multi_auth_user_id' => $multi_auth_user->id,
'email' => $email,
'password' => Hash::make('password'),
'name' => '生徒 ' . $i,
]);
$student->save();
}
}
}
database/seeders/TeacherSeeder.php
<?php
namespace Database\Seeders;
use App\Models\MultiAuthUser;
use App\Models\Teacher;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
class TeacherSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
for($i = 1; $i <= 10; $i++) {
// Multi-Auth
$multi_auth_user = new MultiAuthUser();
$multi_auth_user->save();
// Teacher
$email = 'teacher' . $i . '@example.com';
$teacher = new Teacher([
'multi_auth_user_id' => $multi_auth_user->id,
'email' => $email,
'password' => Hash::make('password'),
'name' => '先生 ' . $i,
]);
$teacher->save();
}
}
}
なお、Seeder
は作成しただけでは有効になりませんので、Laravel
へ登録します。
database/seeders/DatabaseSeeder.php
// 省略
public function run(): void
{
$this->call([
StudentSeeder::class, //
ここ &
TeacherSeeder::class, //
ここ
]);
}
// 省略
では、この状態でデータベースを初期化してみます。
以下のコマンドを実行してください。
php artisan migrate:fresh --seed
すると、実際のテーブルはこうなりました。
では今回は、コントローラーは基本的なCRUD
でOKなので省略します。
お疲れ様でした
テストしてみる
では、実際にテストしてみましょう
なお、今回はPhpUnit
を使ってテストします。(Pest
が主流との噂ですがまだ手を出していません、そのうちこっそり始めましょうね…)
php artisan make:test MessageTest
すると、ファイルが作成されるので中身を次のようにします。
tests/Feature/MessageTest.php
<?php
namespace Tests\Feature;
use App\Models\Message;
use App\Models\Student;
use App\Models\Teacher;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Str;
use Tests\TestCase;
class MessageTest extends TestCase
{
public function test_can_send_messages()
{
$keys = [
'student --> teacher',
'teacher --> student',
'student --> student',
'teacher --> teacher',
];
foreach ($keys as $key) {
$sender = null;
$receiver = null;
if(Str::startsWith($key, 'student')) {
$sender = Student::inRandomOrder()->first();
} else if(Str::startsWith($key, 'teacher')) {
$sender = Teacher::inRandomOrder()->first();
}
if(Str::endsWith($key, 'student')) {
$receiver = Student::inRandomOrder()->first();
} else if(Str::endsWith($key, 'teacher')) {
$receiver = Teacher::inRandomOrder()->first();
}
$message_body = $key .': '. Str::random(); // ランダムなメッセージを生成
$message = $sender->sendMessage($message_body, $receiver);
$this->assertEquals($message->sender_id, $sender->multi_auth_user->id); // 送信者が正しいことを確認
$this->assertEquals($message->receiver_id, $receiver->multi_auth_user->id); // 受信者が正しいことを確認
$this->assertEquals($message->body, $message_body); // メッセージが正しいことを確認
$messages = Message::query()
->where('sender_id', $sender->multi_auth_user->id)
->where('receiver_id', $receiver->multi_auth_user->id)
->get();
$this->assertGreaterThan(0, $messages->count()); // メッセージが1件以上存在することを確認
}
}
}
この中でやっていることは、以下の4つのパターンでメッセージ送信し、その内容があっているかどうかのチェックをしています。
- 生徒 –> 先生 へ送信
- 先生 –> 生徒 へ送信
- 生徒 –> 生徒 へ送信
- 先生 –> 先生 へ送信
では、実際にテストを実行してみましょう。
以下のコマンドを実行してください。
php artisan test --filter MessageTest
すると・・・・・・
はい
うまくテストを通過しました。
成功です。
では、念のためmessages
テーブルがどうなっているかもチェックしておきましょう。
うまくいっているでしょうか・・・・・・
はい
すべてのパターンのメッセージが送信(保存)されているのがわかります。
全て成功です。
後は、以下のようにコントローラー内でログインユーザーに関連するものを引っ張ってくれば、その人用のデータということになります。
<?php
namespace App\Http\Controllers;
use App\Models\Message;
use Illuminate\Http\Request;
class MessageController extends Controller
{
public function index(Request $request)
{
$user = $request->user();
$messages = Message::query()
->where('sender_id', $user->multi_auth_user_id)
->orWhere('receiver_id', $user->multi_auth_user_id)
->get();
dd($messages->toArray());
}
}
企業様へのご提案
今回のようにMulti−Auth
を用いていてもDB
構造のもたせ方次第で今回のメッセージのような「相互に作用するような機能」を持たせることができます。
もし御社のシステムに複数タイプのログインユーザーが必要で、さらにその人たちを連携させたいとお考えの場合は、ぜひお問い合わせからご相談ください。
お待ちしております。
おわりに
ということで、今回はMulti-Auth
を使ったメッセージ機能を作ってみました。
Multi-Auth
はテーブルを分けることができるなど、より明確なDB
構造をつくることができる反面、そうなると連携が複雑になったりもしますが、今回のように親テーブルを使うことで実装ができるようになります。
また、例の「ポリモーフィック関連」についてはあまり経験がないので一概には言えないのですが、記事の最初あたりで書いたように各ユーザーにDeleting
イベントをつけておいて、各ユーザーが削除される直前にすべてのデータを削除してしまう、という流れではダメなのかなと最期になっても思っています。
というのも、ポリモーフィックはLaravel
にもリレーションシップのメソッドがありますし、ChatGPT
に聞いてみたら一番最初に返ってきた答えはポリモーフィックするパターンでした。(しかも、今回の場合だと、ユーザーが消えても相手方は過去メッセージを見られるようにした方がいいとも言え、うーんといったカンジです)
ポリモーフィック関連って、もし新入社員がやってたら注意されちゃったりするんでしょうか。
と、答えの出ない考察でした。(ぜひ詳しい方いい情報がありましたら、教えてくださいm(_ _)m)
ではでは〜
「楽天とかの欲しい物リストって、
気がついたら忘れてて、
売り切れになってません??」