見出し画像

laravel breeze (react) にavater機能を付ける(1)

laravel breezeの説明はAIに任せて割愛する

導入部分も割愛。確かどこかに書いたような…

git clone https://gitlab.com/catatsumuri/laravel10-starter.git -b breeze-inertia-react

ここに一通り設定したものがあるから適当に持ってくるってことで話を進めますぞ。作業ログ

% cp .env.example .env
% docker run --rm -it -v $(pwd):/app composer install --ignore-platform-reqs
% ./vendor/bin/sail up
% ./vendor/bin/sail npm install
% ./vendor/bin/sail artisan migrate:fresh --seed

  Dropping all tables .................................................................................................... 40ms DONE

   INFO  Preparing database.

  Creating migration table ............................................................................................... 38ms DONE

   INFO  Running migrations.

  2014_10_12_000000_create_users_table ................................................................................... 48ms DONE
  2014_10_12_100000_create_password_reset_tokens_table ................................................................... 74ms DONE
  2019_08_19_000000_create_failed_jobs_table ............................................................................. 54ms DONE
  2019_12_14_000001_create_personal_access_tokens_table .................................................................. 81ms DONE


   INFO  Seeding database.

% ./vendor/bin/sail artisan key:gen

   INFO  Application key set successfully.
% ./vendor/bin/sail npm run dev

まあここではlaravel sailを使っているけどsailなんて必要ないって人は単純にそこは省いてもらえばokだろう。

ユーザーを1人作る

database/seeders/DatabaseSeeder.php にあるやつをコメント外すくらいでok

<?php

namespace Database\Seeders;

// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        // \App\Models\User::factory(10)->create();

        \App\Models\User::factory()->create([
            'name' => 'Test User',
            'email' => 'test@example.com',
        ]);
    }
}
artisan migrate:fresh --seed

すればログインできるであろう。パスワードはpasswordである。比較的中級以上向けの文章だから、こんなところで躓いていてはいけない。

つか、ここまで到達できなければこの先は消化試合なような気もするが…

breezeの機能を見ていく

この殺風景なDashboardはまあjsxのテンプレートとしても使えるけど、これはともかく


このドロップダウンメニューからのprofileで提供される情報はbreezeが作成するもので基本的に退会機能も付いている。これを潰したいとかいう場合はまあ各々頑張っていただくとしてまあその、こういうもんなのだ。ここではProfile Informationを拡張してAvatarを作る。こういうのもまあAIで適当にでっち上げてしまった


まず当該のProfileのコード部分を確認しよう

まず、Profileの当該部分を見れば明かなようにこれはEditを即座に提示しているので(要するにShowとかじゃないので)当該viewは resources/js/Pages/Profile/Edit.jsx となる。おそらくコードはspace 4インデントになっているが、以降では2に修正する(まあ紙面の関係もありますし)

import { Head } from '@inertiajs/react';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import DeleteUserForm from './Partials/DeleteUserForm';
import UpdatePasswordForm from './Partials/UpdatePasswordForm';
import UpdateProfileInformationForm from './Partials/UpdateProfileInformationForm';

export default function Edit({ auth, mustVerifyEmail, status }) {
  return (
    <AuthenticatedLayout
      user={auth.user}
      header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">Profile</h2>}
    >
      <Head title="Profile" />

      <div className="py-12">
        <div className="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
          <div className="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
            <UpdateProfileInformationForm
              mustVerifyEmail={mustVerifyEmail}
              status={status}
              className="max-w-xl"
            />
          </div>

          <div className="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
            <UpdatePasswordForm className="max-w-xl" />
          </div>

          <div className="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
            <DeleteUserForm className="max-w-xl" />
          </div>
        </div>
      </div>
    </AuthenticatedLayout>
  );
}

このようにおおきく

  • UpdateProfileInformationForm 

  • UpdatePasswordForm

  • DeleteUserForm

に別れていることが理解できる。変更するのは当然UpdateProfileInformationForm という事になるだろうそれは

import UpdateProfileInformationForm from './Partials/UpdateProfileInformationForm';

が示しているように resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.jsx になる。このソースコードを見ていこう。例によってindentは修正済み

UpdateProfileInformationFormのソースコード

import { Link, useForm, usePage } from '@inertiajs/react';
import { Transition } from '@headlessui/react';
import InputError from '@/Components/InputError';
import InputLabel from '@/Components/InputLabel';
import PrimaryButton from '@/Components/PrimaryButton';
import TextInput from '@/Components/TextInput';

export default function UpdateProfileInformation({ mustVerifyEmail, status, className = '' }) {
  const { user } = usePage().props.auth;

  const {
    data, setData, patch, errors, processing, recentlySuccessful,
  } = useForm({
    name: user.name,
    email: user.email,
  });

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

    patch(route('profile.update'));
  };

  return (
    <section className={className}>
      <header>
        <h2 className="text-lg font-medium text-gray-900">Profile Information</h2>

        <p className="mt-1 text-sm text-gray-600">
          Update your account's profile information and email address.
        </p>
      </header>

      <form onSubmit={submit} className="mt-6 space-y-6">
        <div>
          <InputLabel htmlFor="name" value="Name" />

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

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

        <div>
          <InputLabel htmlFor="email" value="Email" />

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

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

        {mustVerifyEmail && user.email_verified_at === null && (
        <div>
          <p className="text-sm mt-2 text-gray-800">
            Your email address is unverified.
            <Link
              href={route('verification.send')}
              method="post"
              as="button"
              className="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
            >
              Click here to re-send the verification email.
            </Link>
          </p>

          {status === 'verification-link-sent' && (
          <div className="mt-2 font-medium text-sm text-green-600">
            A new verification link has been sent to your email address.
          </div>
          )}
        </div>
        )}

        <div className="flex items-center gap-4">
          <PrimaryButton disabled={processing}>Save</PrimaryButton>

          <Transition
            show={recentlySuccessful}
            enter="transition ease-in-out"
            enterFrom="opacity-0"
            leave="transition ease-in-out"
            leaveTo="opacity-0"
          >
            <p className="text-sm text-gray-600">Saved.</p>
          </Transition>
        </div>
      </form>
    </section>
  );
}

いよいよアップロードフォームを作成する

まず、シンプルな方法でやってみよう。

まずEmailをコピって、、、といいたいところだが

        {mustVerifyEmail && user.email_verified_at === null && (
        <div>
          <p className="text-sm mt-2 text-gray-800">
            Your email address is unverified.
            <Link
              href={route('verification.send')}
              method="post"
              as="button"
              className="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
            >
              Click here to re-send the verification email.
            </Link>
          </p>

          {status === 'verification-link-sent' && (
          <div className="mt-2 font-medium text-sm text-green-600">
            A new verification link has been sent to your email address.
          </div>
          )}
        </div>
        )}

という余計な構文が見える。これはemailのvalidationが完了していない時に出てくる。こういうのもあるんですわ。まあ今回はこれはどうでもいいんだけど、この上に書くとまずいので下にemailの部分をコピってみよう

        <div>
          <InputLabel htmlFor="email" value="Email" />

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

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

        {mustVerifyEmail && user.email_verified_at === null && (
        <div>
          <p className="text-sm mt-2 text-gray-800">
            Your email address is unverified.
            <Link
              href={route('verification.send')}
              method="post"
              as="button"
              className="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
            >
              Click here to re-send the verification email.
            </Link>
          </p>

          {status === 'verification-link-sent' && (
          <div className="mt-2 font-medium text-sm text-green-600">
            A new verification link has been sent to your email address.
          </div>
          )}
        </div>
        )}


{/* コピーされた部分 */}
        <div>
          <InputLabel htmlFor="email" value="Email" />

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

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

すると

こういう感じになるので、これを改造していく。

        <div>
          <InputLabel htmlFor="avatar" value="Avatar" />

          <input
            id="avatar"
            type="file"
            className="mt-1 block w-full"
            onChange={e => setData('avatar', e.target.files[0])}
          />

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

ファイルの場合はまあこんな感じでやるってことで

このようにclassicalなタイプのアップローダーが出現した。ただ、これではuploadできない。inertia.jsの場合はpostする時のキーをちゃんと来めておかないといけない。

export default function UpdateProfileInformation({ mustVerifyEmail, status, className = '' }) {
  const { user } = usePage().props.auth;

  const {
    data, setData, patch, errors, processing, recentlySuccessful,
  } = useForm({
    name: user.name,
    email: user.email,
    avatar: null,
  });

このようにavatarを付け加えた

さて、

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

    patch(route('profile.update'));
  };

これによりprofile.updateにpatchされる。

それは app/Http/Controllers/ProfileController.php である。ここで一度リクエストを停止してみよう

    public function update(ProfileUpdateRequest $request): RedirectResponse
    {
        dd($request->all()); // ダンプして停止
        $request->user()->fill($request->validated());

        if ($request->user()->isDirty('email')) {
            $request->user()->email_verified_at = null;
        }

        $request->user()->save();

        return Redirect::route('profile.edit');
    }

そうすると、画像を送ってないときは


こうなるのに、画像を送ったときは


[] とかいう空配列になり全データーが壊滅状態になる。これはまずinertia.jsのハマりポイントなので解説する(ドキュメントに書いてあるんですけどね)

修正していく

ここで重要な告知があるが、putとかpatchではファイルをアップロードできない(重要)

Uploading files using a multipart/form-data request is not natively supported in some server-side frameworks when using the PUT,PATCH, or DELETE HTTP methods. The simplest workaround for this limitation is to simply upload files using a POST request instead.

However, some frameworks, such as Laravel and Rails, support form method spoofing, which allows you to upload the files using POST, but have the framework handle the request as a PUT or PATCH request. This is done by including a _method attribute in the data of your request.
(AIによる翻訳)

サーバーサイドのフレームワークによっては、PUT、PATCH、またはDELETEのHTTPメソッドを使用している場合に、multipart/form-dataリクエストを使ったファイルのアップロードがネイティブにサポートされていないことがあります。

この制限を回避する最も簡単な方法は、単にPOSTリクエストを使ってファイルをアップロードすることです。しかし、LaravelやRailsのようなフレームワークでは、フォームメソッドのスプーフィングをサポートしており、POSTを使ってファイルをアップロードしながら、フレームワークがリクエストをPUTやPATCHリクエストとして処理することを可能にしています。これは、リクエストのデータに_method属性を含めることで実現されます。

https://inertiajs.com/file-uploads#multipart-limitations

従ってpostをつかう

  const {
    data, setData, post, errors, processing, recentlySuccessful,
  } = useForm({
    name: user.name,
    email: user.email,
    avatar: null,
  });

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

    post(route('profile.update'), {});
  };

しかしこの状態では当然

こうなるのでpatchを偽装する

  const {
    data, setData, post, errors, processing, recentlySuccessful,
  } = useForm({
    name: user.name,
    email: user.email,
    avatar: null,
    _method: "patch",
  });

  const submit = (e) => {
    e.preventDefault();
    post(route('profile.update'));
  };

ドキュメントにあるrouter.patchを使ってもいいけどコードが長くなるので、これでも大丈夫

このように正しくdumpできれば後はbackendの作業となる。ま、長くなったので次回に続く、かどうかはおれの気力次第かw


この記事が気に入ったらサポートをしてみませんか?