laravel11 + inertiaにおけるユーザー管理の例
基本的にはあまり変わってはいない。ただlisnterが完全に自動化されているので最終ログインなどを記録するところを着目する。一応今回は作業ログを残す。
ユーザーコントローラーの作成
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
class UserController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
}
こんなのがあったときに、とりあえず
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class UserController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(): Response
{
$users = User::all();
return Inertia::render('Users/Index', [
'users' => $users,
]);
}
とするのは鉄板は変わらない。
resources/js/Pages/Users/Index.jsx
import { Head } from '@inertiajs/react';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { useLaravelReactI18n } from 'laravel-react-i18n';
export default function UserIndex({ auth }) {
const { t, currentLocale } = useLaravelReactI18n();
return (
<AuthenticatedLayout
user={auth.user}
header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">{t("Users")}</h2>}
>
<Head title={t("Users")} />
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div className="p-6 text-gray-900">
{t("Contents")}
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
);
}
ここではi18nが入っているが、まあそれほど差異は無くできるはずだ
ユーザー一覧を作る
resources/js/Pages/Users/Index.jsx
import { Head } from '@inertiajs/react';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { useLaravelReactI18n } from 'laravel-react-i18n';
export default function UserIndex({ auth, users }) {
const { t, currentLocale } = useLaravelReactI18n();
return (
<AuthenticatedLayout
user={auth.user}
header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">{t("Users")}</h2>}
>
<Head title={t("Users")} />
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div className="p-6 text-gray-900">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{t("Name")}
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{t("Email")}
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{t("Verified At")}
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{users.map((user) => (
<tr key={user.id}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{user.name}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{user.email}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{user.email_verified_at ? user.email_verified_at : t("Not Verified")}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
);
}
時間表示がアレな件
day.jsとかdate-fnsを使うといいかも。
date-fnsを使う例
npm install date-fns
resources/js/Pages/Users/Index.jsx
import { format } from 'date-fns';
import { formatDistanceToNow } from 'date-fns';
import { ja } from 'date-fns/locale';
export default function UserIndex({ auth, users }) {
const { t, currentLocale } = useLaravelReactI18n();
function formatDate(dateString) {
const date = new Date(dateString);
return format(date, 'yyyy年MM月dd日 HH:mm:ss', { locale: ja });
}
function formatTimeAgo(dateString) {
const date = new Date(dateString);
return formatDistanceToNow(date, { addSuffix: true, locale: ja });
}
としておいて
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{user.email_verified_at
? (
<>
{formatDate(user.email_verified_at)}
<span className="text-gray-400">({formatTimeAgo(user.email_verified_at)})</span>
</>
)
: t('Not Verified')
}
</td>
など
とはいえverified atの時間を出してもあんま意味を無さないと思うので、最終ログインフィールドをお勧めしている。
最終ログインフィールド
app/Providers/EventServiceProvider.php
というのがlaravel10まで存在したが消滅した。
artisan make:listener UpdateLastLoginDate --event 'Illuminate\Auth\Events\Login'
このようにevent指定してリスナーを作ると自動的に当該イベントに対してListenerが呼ばれるようになっている。では書いていこう。
<?php
namespace App\Listeners;
use Illuminate\Auth\Events\Login;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class UpdateLastLoginDate
{
/**
* Create the event listener.
*/
public function __construct()
{
//
}
/**
* Handle the event.
*/
public function handle(Login $event): void
{
$event->user->last_login_at = now();
$event->user->save();
}
}
Illuminate\Auth\Events\Loginがわかってないと対応できないが、、まあログインしてみて書き込まれていれば成功である
= App\Models\User {#6288
id: 1,
name: "Test User 1",
email: "user1@example.com",
email_verified_at: "2024-04-17 20:00:03",
last_login_at: "2024-04-17 20:03:26",
#password: "$2y$12$kBPcbjKRhle.pW.A.mnX3Og.8H7yqvfJBuIRRXk0DyPgdN7AJMPeS",
#remember_token: "6kkK82BVEh",
created_at: "2024-04-17 20:00:03",
updated_at: "2024-04-17 20:03:26",
}
一覧に最終ログインを出す
コードは割愛
メールの認証時間は不要だが認証されているかどうかは知りたい。
みたいな奴
npm install react-icons
react-iconsをいれ
import { VscVerifiedFilled, VscUnverified } from 'react-icons/vsc';
呼びこんでくる
まあ面倒なのでコード全部貼って終わります
resources/js/Pages/Users/Index.jsx
import { Head } from '@inertiajs/react';
import { useLaravelReactI18n } from 'laravel-react-i18n';
import { format, formatDistanceToNow } from 'date-fns';
import { ja } from 'date-fns/locale';
import { VscVerifiedFilled, VscUnverified } from 'react-icons/vsc';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
export default function UserIndex({ auth, users }) {
const { t, currentLocale } = useLaravelReactI18n();
function formatTimeAgo(dateString) {
const date = new Date(dateString);
return formatDistanceToNow(date, { addSuffix: true, locale: ja });
}
function formatLastLogin(dateString) {
if (dateString) {
const date = new Date(dateString);
return `${format(date, 'yyyy年MM月dd日 HH:mm:ss', { locale: ja })} (${formatTimeAgo(date)})`;
}
return t('Never logged in');
}
return (
<AuthenticatedLayout
user={auth.user}
header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">{t('Users')}</h2>}
>
<Head title={t('Users')} />
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div className="p-6 text-gray-900">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{t('Name')}
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-16">
{t('Verified')}
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{t('Email')}
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{t('Last Logined At')}
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{users.map((user) => (
<tr key={user.id}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{user.name}</td>
<td className="px-6 py-4 whitespace-nowrap text-center w-16">
{user.email_verified_at ? (
<VscVerifiedFilled className="mx-auto text-green-600" title={t('Verified')} />
) : (
<VscUnverified className="mx-auto text-red-600" title={t('Not Verified')} />
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{user.email}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{formatLastLogin(user.last_login_at)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
);
}
認証のヘッダはちょっとアレだったから変えちゃった。
で、次回はこれをもうちょい改良したいね。