laravelの認証機構を見てみよう(9) : userのcrudを仕上げる (1) - ユーザーの作成

ん〜1日考えてみたんだけど、まあユーザーのcrudくらいは仕上げとくか的な。まあ既にタイトルに偽りありみたいな感じになってきたので終わったら後で変えようかな。てか、案の定これに関しては今回は作成しただけで紙面が一杯一杯になっちゃったので次に続く。9なのにさらに1とか分岐しはじめてもうカオスである。


ユーザーの作成

とりあえずボタンを追加
resources/js/Pages/Users/Index.jsx

Create New Userボタンが追加された
            <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 bg-white border-b border-gray-200 flex justify-end">
                            <Link href={route('users.create')} className="text-blue-600 hover:text-blue-900 flex items-center">
                                <button className="bg-blue-500 hover:bg-blue-700 text-white py-1 px-2 rounded flex items-center">
                                    <VscPersonAdd className="mr-2" />{t("Create New User")}
                                </button>
                            </Link>
                        </div>

ん〜色とかどうなのかな。あとアイコンもまあ適当

言語を追加する
lang/ja.json

    "Create New User": "新規ユーザー作成",

これは、多言語化しようという強い気持ちがある場合はもう本当にソースコードに日本語を書かねえぞくらいの勢いでいかないとまあまあ挫折すると思われ…

createアクション

app/Http/Controllers/UserController.php

    public function create(): Response
    {
        return Inertia::render('Users/Create');
    }

これは今のところ渡す情報は必要なさそう。で

resources/js/Pages/Users/Create.jsx

import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, Link } from '@inertiajs/react';
{/*
import { VscVerifiedFilled, VscUnverified, VscInfo, VscPersonAdd } from "react-icons/vsc";
import { useConfiguredDayjs } from '@/hooks/useConfiguredDayjs';
*/}
import { useLaravelReactI18n } from 'laravel-react-i18n';


export default function UserIndex({ auth }) {
    const { t } = useLaravelReactI18n();

    return (
        <AuthenticatedLayout
            user={auth.user}
            header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">{t("New User")}</h2>}
        >
            <Head title="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>
                </div>
            </div>
        </AuthenticatedLayout>
    );
}

とりあえずババっとガワを作る。そしたらformを置いていく。これは、ユーザーのregistration (resources/js/Pages/Auth/Register.jsx) を多いに参考にする。つかこれも真面目にやるなら多言語化しないといけないね。

resources/js/Pages/Users/Create.jsx

import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, Link, useForm } from '@inertiajs/react';
{/*
import { VscVerifiedFilled, VscUnverified, VscInfo, VscPersonAdd } from "react-icons/vsc";
import { useConfiguredDayjs } from '@/hooks/useConfiguredDayjs';
*/}
import { useLaravelReactI18n } from 'laravel-react-i18n';


import InputError from '@/Components/InputError';
import InputLabel from '@/Components/InputLabel';
import PrimaryButton from '@/Components/PrimaryButton';
import TextInput from '@/Components/TextInput';

export default function UserCreate({ auth }) {
    const { t } = useLaravelReactI18n();

    const { data, setData, post, processing, errors, reset } = useForm({
        name: '',
        email: '',
        password: '',
        password_confirmation: '',
    });

    const submit = (e) => {
        e.preventDefault();

        post(route('users.store'));
    };

    return (
        <AuthenticatedLayout
            user={auth.user}
            header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">{t("Create New User")}</h2>}
        >
            <Head title="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 p-6">
                        <form onSubmit={submit} className="space-y-4">
                            <div>
                                <InputLabel htmlFor="name" value={t("Name")} />
                                <TextInput
                                    id="name"
                                    name="name"
                                    value={data.name}
                                    className="mt-1 block w-full"
                                    onChange={(e) => setData('name', e.target.value)}
                                />

                                <InputError message={errors.name} className="mt-2" />
                            </div>
                            <PrimaryButton className="ml-4" disabled={processing}>
                                {t("Create New User")}
                            </PrimaryButton>
                        </form>
                    </div>
                </div>
            </div>
        </AuthenticatedLayout>
    );
}

こういうのの慣れてないときのコツとしては一度に全部やらないという事。ちょっと作ってみて送信。で,controllerのstoreでddする

    public function store(Request $request)
    {
        dd($request->all());
    }


送信できてはいるようである

nameは必須属性なのでとりあえずvalidationしとく、ってことでRequestを作成する。RequestはStoreとUpdate用に2つ作るのが作法かもしれないが面倒なのでここでは1つにしよう。

form requestの作成

artisan make:request UserRequest

で作ってくれるので

app/Http/Controllers/UserController.php に以下を追記

use App\Http\Requests\UserRequest;
// 略
    public function store(UserRequest $request)
    {
        dd($request->all());
    }

リクエストはデフォルトでall denyになるので以下が正常だ。

が、今はこのメッセージになった。なんかlaravel-langのタイミングでこのエントリーがあったりなかったりするようなしないような…まあ、無い場合は足しといてもいいかも

    "This action is unauthorized.": "禁止されているアクションです",

まあ、いずれにせよこれはRequestの中を変更しないといけない
app/Http/Requests/UserRequest.php

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UserRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

このようにreturn trueに変更する。これはもうテンプレやね。もちろん権限チェックとかをここに書いてもいいんだけど今はmiddleware routeでadminだけってのがキいてるから特に必要ないかも。

で、とりあえずこうしておく

    public function rules(): array
    {
        return [
            'name' => 'required',
        ];
    }

そうするとlangのja.jsonに設定されている翻訳が効いている関係で

このようにエラーメッセージが表示される。ここは「名前は必須項目です。」みたいなよりフレンドリーなメッセージにする事も可能ではあるが基本的にはサーバーサイドで落とすだけなので必要あるまい。最終的にはrequired属性をhtmlに付けておけば通常のオペレーションでこのエラーメッセージを見る事は無いからであーる。

続きの項目の実装

ここまでokくさい場合はどんどん足していく。まあemailとパスワードとパスワード確認項目である。

まずemail

<div className="mt-4">
    <InputLabel htmlFor="email" value={t("Email")} />

    <TextInput
        id="email"
        type="text"
        name="email"
        value={data.email}
        className="mt-1 block w-full"
        onChange={(e) => setData('email', e.target.value)}
    />

    <InputError message={errors.email} className="mt-2" />
</div>

そしたらばvalidationである。ちょっと変更した

    public function rules(): array
    {
        return [
            'name'  => 'required|string|max:255',
            'email' => 'required|email|max:255',
        ];
    }

これもまずシンプルなフォームで定義してあるからこんな感じのを裏で落とすテストをしている

さらにパスワードと確認フィールド

<div className="mt-4">
    <InputLabel htmlFor="password" value={t("Password")} />

    <TextInput
        id="password"
        type="password"
        name="password"
        value={data.password}
        className="mt-1 block w-full"
        autoComplete="new-password"
        onChange={(e) => setData('password', e.target.value)}
    />

    <InputError message={errors.password} className="mt-2" />
</div>

<div className="mt-4">
    <InputLabel htmlFor="password_confirmation" value={t("Confirm Password")} />

    <TextInput
        id="password_confirmation"
        type="password"
        name="password_confirmation"
        value={data.password_confirmation}
        className="mt-1 block w-full"
        autoComplete="new-password"
        onChange={(e) => setData('password_confirmation', e.target.value)}
    />

    <InputError message={errors.password_confirmation} className="mt-2" />
</div>

これに関してはpassword_confirmationはvalidateで使うだけである。

use Illuminate\Validation\Rules;
// 略
    public function rules(): array
    {
        return [
            'name'  => 'required|string|max:255',
            'email' => 'required|email|max:255',
            'password' => ['required', 'confirmed', Rules\Password::defaults()],
        ];
    }


とまあこんな具合になるだろう。ただ、このパスワードフィールドに関してはフロントエンドだけでは完結できないのでこのメッセージは改変した方がいいかもしれませんな。

てかRules\Password::defaults()って何ぞ

これはlaravel8.4くらいでひっそり追加された機能であーる。

// 最低8文字必要
Password::min(8)

// 最低1文字の文字が必要
Password::min(8)->letters()

// 最低大文字小文字が1文字ずつ必要
Password::min(8)->mixedCase()

// 最低一文字の数字が必要
Password::min(8)->numbers()

// 最低一文字の記号が必要
Password::min(8)->symbols()

みたいな。ただ、これに関してだけはどうしてもvalidationメッセージが必要になるが、定義するのも面倒だから、まあこれでいいか…

ちなみにパスワードの定義はデフォルトを app/Providers/AppServiceProvider.php に記述可能である

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Rules\Password;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        //
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        Password::defaults(function () {
            return Password::min(8)->mixedCase();
        });
    }
}


laravel-langの定義もある程度カバーしてくれるしまあこれはこれでいいかとなりませんか?あたしゃ、なりますね。

ちなみに

    public function boot(): void
    {
        Password::defaults(function () {
            return Password::min(8)->mixedCase()->uncompromised();
        });
    }

uncompromised()を付けると適当なパスワードは通らなくなるぞ(ひえー)

外部サービスに問い合わせる漏洩チェック

ちなみに

import { useEffect } from 'react';

export default function UserCreate({ auth }) {

    useEffect(() => {
        return () => {
            reset('password', 'password_confirmation');
        };
    }, []);

このuseEffectは何で付いてるかよくわからんのだけどwまあ付けといた方がいいんじゃないかな。。。

実際に保存してindexに引き渡す

じゃあ、やってみよう。あとはもうそんな難しい事もないけど、とりあえずユーザーモデル
app/Models/User.php

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

このfillableは最初から書かれているはずであり、これが書いてないと保存されないので一応確認

続いてstore
app/Http/Controllers/UserController.php

    public function store(UserRequest $request): RedirectResponse
    {
        $data = $request->validated();
        User::create($data);

        return redirect(route('users.index'))
            ->with('success', __('New User Created'))
        ;
    }

validated()でバリデーション済みのデーターが落ちてくるつまりvalidationされていないフィールドは保存されないという保険が効く、のと、あとpassword_confirmationが削除される利点もあるっちゃある。

でまあ保存すると

ユーザーが作成されるが、emailがvalidationされていない。これはちょっと面倒なので、強制的にvalidation済みにしといてもいいかも。

これはまずfillableに追加する必要がある

    protected $fillable = [
        'name',
        'email',
        'password',
        'email_verified_at',
    ];

そしたら以下の通りつかパスワードのハッシュ化を忘れてたので入れた

public function store(UserRequest $request): RedirectResponse
{
    $data = $request->validated();

    // email_verified_atを設定
    $data['email_verified_at'] = now();

    // パスワードをハッシュ化
    $data['password'] = bcrypt($data['password']);

    // ユーザーを作成
    User::create($data);

    // 成功メッセージと共にusers.indexにリダイレクト
    return redirect(route('users.index'))
        ->with('success', __('New User Created'));
}

重複フィールドの問題

で今

  • a@a.a

  • test@test.test

みたいな2つのメールアドレスが入ったんだけど、これをさらに新規追加しようとすると…

DBエラーになって非常によくないんで、validationで落とす

    public function rules(): array
    {
        return [
            'name'     => 'required|string|max:255',
            'email'    => 'required|string|email|max:255|unique:'.User::class,
            'password' => ['required', 'confirmed', Rules\Password::defaults()],
        ];
    }


このメッセージもどうしても露出しちゃうんだけど、まあこれでいいか…気になる場合はvalidationドキュメントのメッセージの変更を確認してみて!

breadcrumbの新設

まあこれは

routes/breadcrumbs.php

Breadcrumbs::for('users.create', function(BreadcrumbTrail $trail)
{
    $trail->parent('users.index');
    $trail->push(__('Create'), route('users.create'));
});

こんなのを書き足すだけ

lang/ja.json に加えとけば、日本語にはなるだろう

    "Create": "作成",

まとめ

つーかね、作成だけで終わるよねこれは。次回は更新とか、削除もできたらええなあ…


いいなと思ったら応援しよう!