Laravel Multi-Auth なユーザー間でメッセージ送信できるようにする

こんにちは。フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、この間このブログで以下の記事を公開しました。

📝 参考ページ: 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ファイルが作成されることになりますので、中身をそれぞれ変更していきましょう。

※ なお、構造としては、MultiAuthUserStudentTeacherの親テーブルになり、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 で共通部分をつくる

先ほどのStudentTeacherモデルにはどちらにもメッセージ送信ができる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;
    }
} 

なお、このトレイトはすでにStudentTeacherモデルにセット済です。

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);
    }
}

※ ちなみにEnumPHP 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 の中身

では、自動でStudentTeacherの「テストデータ」が作れるように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構造のもたせ方次第で今回のメッセージのような「相互に作用するような機能」を持たせることができます。

もし御社のシステムに複数タイプのログインユーザーが必要で、さらにその人たちを連携させたいとお考えの場合は、ぜひお問い合わせからご相談ください。

お待ちしております。😄✨

開発のご依頼お待ちしております
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ

おわりに

ということで、今回はMulti-Authを使ったメッセージ機能を作ってみました。

Multi-Authはテーブルを分けることができるなど、より明確なDB構造をつくることができる反面、そうなると連携が複雑になったりもしますが、今回のように親テーブルを使うことで実装ができるようになります。

また、例の「ポリモーフィック関連」についてはあまり経験がないので一概には言えないのですが、記事の最初あたりで書いたように各ユーザーにDeletingイベントをつけておいて、各ユーザーが削除される直前にすべてのデータを削除してしまう、という流れではダメなのかなと最期になっても思っています。

というのも、ポリモーフィックはLaravelにもリレーションシップのメソッドがありますし、ChatGPTに聞いてみたら一番最初に返ってきた答えはポリモーフィックするパターンでした。(しかも、今回の場合だと、ユーザーが消えても相手方は過去メッセージを見られるようにした方がいいとも言え、うーんといったカンジです🤔)

ポリモーフィック関連って、もし新入社員がやってたら注意されちゃったりするんでしょうか。

と、答えの出ない考察でした。(ぜひ詳しい方いい情報がありましたら、教えてくださいm(_ _)m)

ではでは〜❗

「楽天とかの欲しい物リストって、
気がついたら忘れてて、
売り切れになってません??」

このエントリーをはてなブックマークに追加       follow us in feedly