
九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、私は時間を見つけて個人的にサイトを作っています。
そして、その経験の中ホントに難しいと感じているのが、
ユーザー登録してもらうこと
です。
もちろんマーケティングの知識が乏しいのは承知の上ですが、やはり個人的なサイトはデザインなどにそれほどお金をかけることもできず「ユーザー登録」してもらうまでの壁が結構高いのが私の印象です。
となると、ログインしなくてもサイトを利用してもらうために何が必要かいくつか考えたのですが、ひとつのアイデアが浮かびました。
それが・・・・・・
登録してない人でも 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 してない」で分岐し、それぞれDB
とCookie
からデータを取得するようにしています。
edit()
ここはユーザーが実際にプリファレンスの選択をし、保存をするページです。
今回はテストですので、theme
で「ライトモード or ダークモード」が選択できるようにします。
update()
ここは、edit()
で選択されたプリファレンスが送信されて来て、実際に保存する場所です。
そのため、ここもログインの有無で分岐し、DB
とCookie
へそれぞれデータを保存するようになっています。
ルートをつくる
続いてはルートです。
先ほどのコントローラーで作成したメソッドを登録してください。
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);
}
}
}
この中でやっていることは次のとおりです。
- Cookieデータの中から、「preferences:」で始まるものだけを取得
- そのデータをループし、key と value を DB へ保存
- 同時にもう Cookie は不要なので、削除
なお、Cookie
の削除は以下のように、forget
をqueue
へセットするようになっています。(最初、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・スマホ間で同じデータが使うためにユーザー登録が必要なことを知らせることでよりユーザー登録の数を増やせるのではないでしょうか。
もしそういった機能をご希望でしたら、ぜひお問い合わせからご相談ください。
お待ちしております。
おわりに
ということで、今回は「Laravel 10.x + React」で機能を実装してみました。
ユーザー登録はシンプルにはいかないので、いろいろと作戦が必要になってくると思いますが、個人的にはまだまだマーケティングの知識は不足しているので、ChatGPT 4
で少しでも補いながら作業を進めています。
とはいえ、知識がゼロの私にとってはChatGPT
は強力すぎる助っ人なのでとても助かりますね。(特にマーケティングとデザイン)
しかし、逆に本業のプログラムとなると、「いや、そこはそうじゃないでしょ…」となる部分も多いことを考えると、マーケティングやデザインにもそういうった部分が少なからず含まれていて、知識がないのでそれを見逃しているという状況と言っていいでしょう。
これからも精進が必要ですね。
ではでは〜
「カナダの友人も
プログラマなのですが、
ChatGPTについて同意見でした」