🤳

LaravelでDiscordと連携したユーザー仮登録機能を作ってみた

に公開

LaravelとDiscordを連携してユーザーの仮登録を行う機能を実装した話をまとめます。

実行環境

PHP 8.3
Laravel 11
MySQL 8.0
JavaScript (fetch API使用)

実装の流れ

  1. 新規登録画面でDiscord IDを入力
  2. 16桁の確認コードを生成
  3. DiscordのDMで確認コードを送信
  4. 確認コードと共に本登録画面へ遷移
  5. 確認コードを登録テーブルと照合して認証

マイグレーションとモデルの作成

Discord IDと登録コードを保存するテーブルを作成します。

php artisan make:model TempRegisterCode -m

マイグレーション

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
    public function up(): void
    {
        Schema::create('temp_register_codes', function (Blueprint $table) {
            $table->id();
            $table->string('discord_id')->comment('DiscordのID'); 
            $table->string('register_code', 255)->comment('ハッシュ化をするため255文字にしておく');
            $table->date('expires_at')->comment('有効期限(日付のみ)');
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('temp_register_codes');
    }
};

モデル

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class TempRegisterCode extends Model
{
    use HasFactory;

    protected $table = 'temp_register_codes';

    protected $fillable = [
        'discord_id',
        'register_code',
        'expires_at',
    ];

    protected $dates = ['expires_at'];
}

コントローラーからDiscordAPIを使用する

Discordに送信ボタンを押下するとAPIを叩きDiscordにbotからメッセージが来ます。
同時にDiscord IDと登録コードをテーブルに保存します。

    public function sendDiscordRegisterCode(Request $request)
    {   
        $discordId = $request['discord-ID'];

        session(['discord_ID' => $discordId]);

        //ランダムな16桁のコードを生成
        $registerCode = substr(str_shuffle('1234567890abcdefghijklmnopqrstuvwxyz'), 0, self::LENGTH);

        //ここでサービスを使用してランダムなコードをディスコードのidあてに送ります
        $this->discordService->sendDiscordRegisterCode($discordId, $registerCode);

        // 翌日の日付を取得
        $expiresAt = $this->userService->getTomorrowDate();

        // データを保存
        $this->userService->createTempRegisterCode($discordId, $registerCode, $expiresAt);

        //新規登録画面2を送る
        return redirect()->route('register2', ['discord-ID' => $discordId]);
    }

サービス

    public function sendDiscordRegisterCode($discordUserId, $registerCode)
    {
        $message = "あなたの登録コードは `{$registerCode}` です。\n\n"
        . "会員登録画面でこのコードを入力してください。\n"
        . "コードの有効期限: 明日まで";

        return $this->sendDirectMessage($discordUserId, $message);
    }

public function sendDirectMessage($discordUserId, $message)
    {
        $channelId = $this->getDmChannelId($discordUserId);
        if (!$channelId) {
            Log::error("チャンネル ID の取得に失敗: ユーザーID {$discordUserId}");
        }
    
        $response = Http::withHeaders([
            'Authorization' => 'Bot ' . $this->botToken,
            'Content-Type' => 'application/json',
        ])->post("https://discord.com/api/v10/channels/{$channelId}/messages", [
            'content' => $message,
        ]);
    
        if (!$response->successful()) {
            Log::error("メッセージ送信失敗: " . $response->body());
            return [
                'success' => false,
                'message' => 'Discord へのメッセージ送信に失敗しました'
            ];
        }
    }

上記のようにしてDiscordに登録コードを送ります。

登録コードの検証

次の画面でユーザー情報とDiscord IDと登録コードを入力して送信します。

バリデーション

public function rules(): array
    {
        return [
            'userid' => 'required|unique:users', // ユーザーIDが必須 & ユニーク
            'term' => 'required|exists:terms,id', // term_idが存在するかチェック
            'password' => 'required|min:8|confirmed', // 8文字以上 & 確認用と一致
            'discord-ID' => 'required|string', // Discord ID は必須の文字列
            'register-code' => [
                'required',
                'string',
                'size:16',
                new ValidDiscordRegisterCode // カスタムバリデーションを適用
            ],
            'user-profile-image' => 'nullable|image|mimes:jpg,jpeg,png,gif|max:2048', // 画像は必須ではなく、最大2MBまで
        ];
    }

Rules

今後再利用するかもしれないと考えカスタムバリデーションを使いました。

class ValidDiscordRegisterCode implements Rule
{
    public function passes($attribute, $value)
    {

        // discord_idに基づいてすべてのレコードを取得
        $tempRegisterCodes = TempRegisterCode::where('discord_id', request()->input('discord-ID'))->get();

        // 取得したすべてのコードに対してチェック
        foreach ($tempRegisterCodes as $tempRegisterCode) {
            // ハッシュ化されたコードと入力されたコードを比較
            if (Hash::check($value, $tempRegisterCode->register_code)) {
                return true; // 一致したらtrueを返す
            }
        }

        return false; // 一致しなければfalseを返す
    }

    public function message()
    {
        return 'Discord ID または 登録コードが無効です。';
    }
}

また、カスタムバリデーションを使用したことでコードの可読性もあがりました。
上記のバリデーションが通ったらユーザー登録をするようなコードにしました。

まとめ

できたら便利だなと思っていたDiscordAPIを使用してみました。実際に使用できてとても感動しました!また、ロジックはサービスにまとめる、カスタムバリデーションを使用するなどコードをすっきり書き可読性を上げることもできました。今後もわかりやすくすっきりとしていながらもしっかりしたコードを書けるようにしたいです。

Discussion