【Laravel 10.x】ログインなしでもデータ保持できて、ユーザー登録したらDBへ移行する機能

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

さてさて、私は時間を見つけて個人的にサイトを作っています。
そして、その経験の中ホントに難しいと感じているのが、

ユーザー登録してもらうこと

です。

もちろんマーケティングの知識が乏しいのは承知の上ですが、やはり個人的なサイトはデザインなどにそれほどお金をかけることもできず「ユーザー登録」してもらうまでの壁が結構高いのが私の印象です。

となると、ログインしなくてもサイトを利用してもらうために何が必要かいくつか考えたのですが、ひとつのアイデアが浮かびました。

それが・・・・・・

登録してない人でも Cookie でデータを保持し、もし登録したらそのデータをDBに引き継ぐ

機能です。

つまり、最初はブラウザにデータを保持しておいてサイトを気に入ってもらえたら「ユーザー登録&データ引き継ぎ」ができるようにしたいというわけです。

そこで❗
今回はこの機能を「Laravel 10.x + React」で実装してみます。

ぜひ何かの参考になりましたら嬉しいです。😄

「カナダの友人が
一回だけカラオケで聞いた
ちびまる子ちゃんの歌を覚えてました😄」

開発環境: Laravel 10.x、React、Inertia.js、TailwindCSS、ChatGPT 4(ベースのみ)

前提として

Laravel 10.xにログイン機能がインストールされていることが前提です。

もしまだの方は、以下のページを参考にしてください。(実際にはLaravel 8.xですが同じくインストールできるはずです👍)

📝 参考ページ: Laravel Breezeで「シンプルな」ログイン機能をインストール

やりたいこと

今回は以下の条件でユーザーの「プリファレンス」(好みの設定。例えば、ダークモードを使う or 使わないなどの設定)のデータを取得できるようにします。

つまり、条件は以下のようになります。

  • ログインしていたらデータベースからデータ取得する
  • ログインしていなかったら Cookie からデータ取得する

では楽しんでやってみましょう❗

マイグレーション&モデルをつくる

まずはDBまわりの準備から始めます。

今回テーブルは「preferences」にしますので、以下のコマンドを実行してください。

php artisan make:model Preference -m

すると、モデルとマイグレーションのファイルが作成されるので中身をそれぞれ以下のように変更します。

app/Models/Preference.php

<?php

namespace App\Models;

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

class Preference extends Model
{
    use HasFactory;

    protected $guarded = ['id']; // 👈 ここを追加しました
}

database/migrations/YYYY_MM_DD_HHMMSS_create_preferences_table.php

public function up()
{
    Schema::create('preferences', function (Blueprint $table) {
        $table->id();
        $table->unsignedBigInteger('user_id')->index();
        $table->string('key');
        $table->text('value');
        $table->timestamps();

        ​$table->foreign('user_id')->references('id')->on('users');
        $table->unique(['user_id', 'key']);
    });
}

では、この状態でデータベースを再構築してみましょう。

以下のコマンドを実行してください。

php artisan migrate:fresh --seed

すると、実際のテーブルはこうなりました。

DB / Cookie からデータ取得する

では、以下の条件でデータを取得できるようにします。

  • ログインしていたらデータベースから
  • ログインしていなかったら Cookie から

今回は、Ajaxでデータを取得するようにしたいので、専用のコントローラーをつくることにします。

以下のコマンドを実行してください。

php artisan make:controller PreferenceController

すると、コントローラーが作成されるので中身を以下のように変更します。

app/Http/Controllers/PreferenceController.php

<?php

namespace App\Http\Controllers;

use App\Models\Preference;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cookie;
use Inertia\Inertia;

class PreferenceController extends Controller
{
    public function index(Request $request)
    {
        // バリデーションは省略しています

        $preference_keys = $request->input('keys', []);

        if(count($preference_keys) === 0) {

            return [];

        }

        $preferences = [];

        if (auth()->check()) {

            $preferences = Preference::query()
                ->where('user_id', auth()->id())
                ->whereIn('key', $preference_keys)
                ->pluck('value', 'key')
                ->toArray();

        } else {

            foreach ($preference_keys as $preference_key) {

                $cookie_key = 'preferences:'. $preference_key;
                $cookie_value = Cookie::get($cookie_key);

                if (! is_null($cookie_value)) {

                    $preferences[$preference_key] = $cookie_value;

                }

            }
        }

        return $preferences;
    }

    public function edit()
    {
        return Inertia::render('Preferences/Edit');
    }

    public function update(Request $request)
    {
        // バリデーションは省略しています

        $results = [];
        $preferences = $request->input('preferences', []);
        $response = response();

        if (auth()->check()) {

            foreach ($preferences as $key => $value) {

                $preference = Preference::firstOrNew([
                    'user_id' => auth()->id(),
                    'key' => $key,
                ]);
                $preference->user_id = auth()->id();
                $preference->key = $key;
                $preference->value = $value;
                $results[] = $preference->save();

            }

        } else {

            foreach ($preferences as $key => $value) {

                $cookie_key = 'preferences:'. $key;

                try {

                    $minutes = 525600; // 1年
                    Cookie::queue($cookie_key, $value, $minutes);

                    $results[] = true;

                } catch (\Exception $e) {

                    $results[] = false;

                }

            }

        }

        $result = ! in_array(false, $results, true);

        return $response->json(['result' => $result]);
    }
}

この中でやっていることは次のとおりです。

index()

ここはajaxを通してプリファレンスのデータを取得する部分です。

そのため、この中では「ログインしている or してない」で分岐し、それぞれDBCookieからデータを取得するようにしています。

edit()

ここはユーザーが実際にプリファレンスの選択をし、保存をするページです。
今回はテストですので、themeで「ライトモード or ダークモード」が選択できるようにします。

update()

ここは、edit()で選択されたプリファレンスが送信されて来て、実際に保存する場所です。

そのため、ここもログインの有無で分岐し、DBCookieへそれぞれデータを保存するようになっています。

ルートをつくる

続いてはルートです。
先ほどのコントローラーで作成したメソッドを登録してください。

routes/web.php

// 省略

use App\Http\Controllers\PreferenceController;

Route::post('/preferences', [PreferenceController::class, 'index'])->name('preferences.index');
Route::get('/preferences/edit', [PreferenceController::class, 'edit'])->name('preferences.edit');
Route::put('/preferences', [PreferenceController::class, 'update'])->name('preferences.update');

ユーザー登録したら Cookie 内のデータをDBへ移行するイベントリスナーをつくる

続いて、ユーザー登録したときにデータ移行する部分です。

直接コントローラーにコードを書いてもいいのですが、せっかくRegisteredイベントがあるので、そこに専用リスナーをセットして動くようにしてみます。

以下のコマンドを実行してください。

php artisan make:listener StorePreferencesFromCookie

すると、イベントファイルが作成されるので中身を次のように変更します。

app/Listeners/StorePreferencesFromCookie.php

<?php

namespace App\Listeners;

use App\Models\Preference;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Cookie;
use Illuminate\Support\Str;

class StorePreferencesFromCookie
{
    /**
     * Create the event listener.
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     */
    public function handle(object $event): void
    {
        $all_cookies = Cookie::get();
        $cookie_preferences = array_filter($all_cookies, function ($key) {

            return Str::startsWith($key, 'preferences:');

        }, ARRAY_FILTER_USE_KEY);

        if (count($cookie_preferences) > 0) {

            $preferences = [];
            $now = now();

            foreach ($cookie_preferences as $key => $value) {

                $original_key = str_replace('preferences:', '', $key);
                Cookie::queue(Cookie::forget($key)); // Cookie を削除

                $preferences[] = [
                    'user_id' => $event->user->id,
                    'key' => $original_key,
                    'value' => $value,
                    'created_at' => $now,
                    'updated_at' => $now,
                ];

            }

            Preference::insert($preferences);

        }
    }
}

この中でやっていることは次のとおりです。

  1. Cookieデータの中から、「preferences:」で始まるものだけを取得
  2. そのデータをループし、key と value を DB へ保存
  3. 同時にもう Cookie は不要なので、削除

なお、Cookieの削除は以下のように、forgetqueueへセットするようになっています。(最初、Cookie::forget($key);単体でうまくいかず少しハマってしまいました…😫)

Cookie::queue(Cookie::forget($key)); // 👈 こうする

なお、イベントリスナーは作成しただけでは実行できませんので、以下のように登録します。

app/Providers/EventServiceProvider.php

use App\Listeners\StorePreferencesFromCookie;

// 省略

protected $listen = [
    Registered::class => [
        SendEmailVerificationNotification::class,
        StorePreferencesFromCookie::class, // 👈ここを追加しました
    ],
];

React 部分をつくる

では、ビューとなるReact部分をつくっていきましょう。
以下のファイルを作成してください。

resources/js/Pages/Preferences/Edit.jsx

import React, {useEffect, useState} from 'react';

export default function Preferences() {

    // 入力
    const [preferences, setPreferences] = useState({
        theme: '',
    });
    const getPreferences = () => {

        const url = route('preferences.index');
        const params = {
            keys: Object.keys(preferences),
        };

        axios.post(url, params)
            .then(response => {

                setPreferences(response.data);

            });

    };
    useEffect(() => {

        getPreferences();

    }, []);

    // 固定データ
    const themes = [
        { value: 'light', label: 'ライトモード' },
        { value: 'dark', label: 'ダークモード' },
    ];

    // イベント
    const handleInputChange = (e) => {

        const { name, value } = e.target;
        setPreferences({ ...preferences, [name]: value });

    };
    const handleSubmit = () => {

        if(confirm('プリファレンスを保存します。よろしいですか?')) {

            const url = route('preferences.update');
            const params = {
                _method: 'PUT',
                preferences: preferences,
            };

            axios.post(url, params)
                .then(response => {

                    setPreferences(response.data);
                    getPreferences();

                });

        }

    };

    return (
        <div className="min-h-screen bg-gray-100 py-6 flex flex-col justify-center sm:py-12">
            <div className="relative py-3 sm:max-w-xl sm:mx-auto">
                <div className="w-80 relative px-4 py-10 bg-white mx-8 md:mx-0 shadow rounded-2xl sm:p-10">
                    <h1 className="text-center text-xl font-semibold mb-4">プリファレンス</h1>
                    <div className="flex items-center mb-5">
                        <label htmlFor="darkmode" className="w-32 font-semibold text-gray-700">テーマ:</label>
                        <select
                            id="theme"
                            name="theme"
                            value={preferences.theme}
                            onChange={handleInputChange}
                            className="block mt-1 w-full p-2 rounded border-gray-300 focus:ring focus:ring-offset-0"
                        >
                            {themes.map((theme) => (
                                <option key={theme.value} value={theme.value}>
                                    {theme.label}
                                </option>
                            ))}
                        </select>
                    </div>
                    <button
                        type="button"
                        className="w-full flex justify-center items-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
                        onClick={handleSubmit}
                    >
                        保存する
                    </button>
                </div>
            </div>
        </div>
    );

};

この中ではデータを選択し、保存するようになっているので特別なコードはありませんが、(今更ながら知った)useStateをオブジェクトで管理し、その中身をセットする以下の部分が気に入っています。(この書き方、スタイリッシュですね😄)

setPreferences({ ...preferences, [name]: value });

テストしてみる

では、実際にテストしてみましょう❗

まずviteを起動して、「https://******/preferences/edit」へブラウザでアクセスします。(ログインしている場合はログアウトしておいてください)

すると、以下のようにテーマを選択するセレクトボックスが表示されます。

これを「ダークモード」に変更して保存ボタンをクリックします。

すると、(ログインしていないので)Cookieの中にデータが格納されました。

もちろん、まだデータベースには何も保存されていません。

では、登録する前にページをリロードして「ダークモード」がCookieによってキープできるか確認しておきます。

はい❗
ちょっとわかりにくいかもしれませんが、うまくキープできることを確認できました。

では、この状態でユーザー登録をしてみましょう。

どうなるでしょうか・・・・・・

まずはDBの方です。

はい❗
先ほどCookieにセットしたdarkという値が保存されています。

不要になったCookieが削除されているかもチェックしておきましょう。

はい❗
消えています。

では、最後に先ほどのページに戻って「ダークモード」がキープされているか(Cookieを使わずDBからデータが取得できているか)を確認しておきましょう。

どうなるでしょうか・・・・・・

はい❗
同じく見た目は同じですが(DBからデータ取得できて)「ダークモード」がキープされています。

すべて成功です😄✨

企業様へのご提案

今回の機能を使えば、ユーザー登録をためらっているけれどもサイトに興味をもっている人がお試しで使えるようになり、結果としてユーザー登録へつなげるようアプローチできます。

なお、Cookieだけでもサイトを使えてしまうので、有効期限を1ヶ月にして強制的にデータが削除されるようにする、もしくはPC・スマホ間で同じデータが使うためにユーザー登録が必要なことを知らせることでよりユーザー登録の数を増やせるのではないでしょうか。

もしそういった機能をご希望でしたら、ぜひお問い合わせからご相談ください。

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

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

おわりに

ということで、今回は「Laravel 10.x + React」で機能を実装してみました。

ユーザー登録はシンプルにはいかないので、いろいろと作戦が必要になってくると思いますが、個人的にはまだまだマーケティングの知識は不足しているので、ChatGPT 4で少しでも補いながら作業を進めています。

とはいえ、知識がゼロの私にとってはChatGPTは強力すぎる助っ人なのでとても助かりますね。(特にマーケティングとデザイン)

しかし、逆に本業のプログラムとなると、「いや、そこはそうじゃないでしょ…」となる部分も多いことを考えると、マーケティングやデザインにもそういうった部分が少なからず含まれていて、知識がないのでそれを見逃しているという状況と言っていいでしょう。

これからも精進が必要ですね。

ではでは〜❗

「カナダの友人も
プログラマなのですが、
ChatGPTについて同意見でした👍」

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