0️⃣

【Laravel】画像アップロード、管理者権限、パスワード確認欄、ページングを追加する

2024/06/03に公開

はじめに

CRUD機能を追加した状態として進めます。
https://zenn.dev/nenenemo/articles/071a8fc8bea5bd

画像をアップロードしてユーザーアイコンの設定、パスワード確認欄、管理者権限、ソフトデリート、認証機能を追加したいと思います。

初回の管理者権限についてはブラウザからは選択できないようにしています。

マイグレーション作成

usersテーブルを更新するマイグレーションを作成してください。

php artisan make:migration add_columns_to_users_table --table=users

マイギュレーションの詳しい内容はこちらの記事にまとめています。
https://zenn.dev/nenenemo/articles/10a3605c037ab6

管理者フラグ(is_admin)がboolean型ではない理由は、string型を使用すると、1には削除権限あり、2には編集権限あり、0は全ての権限ありなどのように将来的に管理者の種類をさらに細かく区分することが可能なためです。

database/migrations/2024_05_29_181223_add_columns_to_users_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('image')->nullable();
            $table->string('is_admin');
            $table->softDeletes();
            $table->dropUnique('users_email_unique');
            $table->unique(['email', 'deleted_at'], 'users_email_unique');
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('image');
            $table->dropColumn('is_admin');
            $table->dropSoftDeletes();
            $table->dropUnique('users_email_unique');
            $table->unique('email', 'users_email_unique');
        });
    }
};

softDeletes

テーブルにdeleted_atカラムを追加するためのものです。deleted_atカラムは、ソフトデリート機能を実装するために使用されます。

これによって削除した場合、deleted_atカラムに削除した時点のタイムスタンプが値に保存されるようになっています。

ソフトデリートとは、データベースからレコードを物理的に削除するのではなく、削除されたことを示すために特定のフラグやカラムを設定する方法です。
https://readouble.com/laravel/11.x/ja/eloquent.html

dropUnique('users_email_unique')

データベースのテーブルから特定のタイプのインデックス情報(制約)を削除する方法の一つです。
https://laravel.com/docs/11.x/migrations#dropping-indexes

インデックスとは、データベース内のテーブルにおいて、検索の高速化やデータの一貫性を確保するための制約です。

今回は、emailカラムに同じメールアドレスの重複を防ぐために使用(一意性制約)されていた条件を下記の理由から一度削除しています。

ソフトデリートを追加することで、削除されたデータはdeleted_atにタイムスタンプが値に保存されてデータベースに残るようになります。

しかし、ソフトデリート追加前に設定した一意性制約がemailカラムに設定されている場合、deleted_atの値に関わらず、同じemailの値を持つ新しいレコードの挿入が許可されず、ソフトデリートされたユーザーのemailと同じ値を登録しようとするとエラーが発生してしまいます。

これを解決するためにdropUnique('users_email_unique')で一度、ソフトデリート追加前に設定した一意性制約を削除した後にdeleted_atを含めた新しい一意性制約を設定することが必要になります。

マイグレーションの実行

マイグレーションを実行してテーブルにカラムの追加を行ってください。

// sailを使用している場合
sail artisan migrate

コンテナ内でマイグレーションを実行する場合はコンテナ内に入ってから下記を実行してください。

php artisan migrate

マイグレーションファイルの内容がデータベースに反映されます。設定した通りにテーブルが作成されているか確認してください。

制約を表示する

SHOW INDEXES FROM <データベース名>.<テーブル名>;

下記はデータベースのlaravelスキーマ内にあるusersテーブルのインデックスを表示するMySQLのSQLコマンドです。

SHOW INDEXES FROM laravel.users;

https://dev.mysql.com/doc/refman/8.0/ja/show-index.html

ページングの作成

ダミーデータの挿入

ファクトリの設定

今回は既に作成してある、database/factories/UserFactory.phpを使用しましたが作成するには次のコマンドを実行してください。

php artisan make:factory UserFactory --model=User

下記のようにモデルに合わせて内容を変更してください。

database/factories/UserFactory.php
<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
 */
class UserFactory extends Factory
{
    /**
     * The current password being used by the factory.
     */
    protected static ?string $password;

    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'name' => fake()->name(),
            'email' => fake()->unique()->safeEmail(),
            'email_verified_at' => now(),
            'password' => static::$password ??= Hash::make('password'),
            'remember_token' => Str::random(30),
            'is_admin' => '0',
        ];
    }

    /**
     * Indicate that the model's email address should be unverified.
     */
    public function unverified(): static
    {
        return $this->state(fn (array $attributes) => [
            'email_verified_at' => null,
        ]);
    }
}

https://laravel.com/docs/11.x/database-testing#model-factories

シーダーの作成

今回は既に作成してある、database/seeders/DatabaseSeeder.phpを使用しましたが作成するには次のコマンドを実行してください。

php artisan make:seeder UsersTableSeeder

下記のようにモデルに合わせて内容を変更してください。

database/seeders/DatabaseSeeder.php
<?php

namespace Database\Seeders;

use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

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

        User::factory()->create([
            'name' => 'Test User',
            'email' => 'test@example.com',
        ]);
    }
}

定義したシーダーを実行して、データベースに30件の新しいユーザが挿入されているか確認してください。

sail artisan db:seed --class=DatabaseSeeder

https://laravel.com/docs/11.x/database-testing#running-seeders

リクエストの修正

登録用のリクエスト

app/Http/Requests/User/UserRequest.php
<?php

namespace App\Http\Requests\User;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

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

    /**
     * Get the validation rules that apply to the request.
     */
    public function rules(): array
    {
        return [
            'name' => 'required|string|max:255',
            'email' => [
                'required',
                'string',
                'email',
                'max:255',
                Rule::unique('users')->whereNull('deleted_at'),
            ],
            'password' => ['required', 'string', 'min:8', 'regex:/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[@$!%*?&])/', 'confirmed'],
            'image' => [
                'image',
                'max:1024',
                'mimes:jpg,png',
            ],
            'is_admin' => 'nullable|string|max:10',
        ];
    }

    /**
     * Get the error messages for the defined validation rules.
     */
    public function messages(): array
    {
        return [
            'password.regex' => 'パスワードは少なくとも1つの半角英字、数字、および記号(@$!%*?&)を含む必要があります。',
        ];
    }
}

更新用のリクエスト

app/Http/Requests/User/UpdateUserRequest.php
<?php

namespace App\Http\Requests\User;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class UpdateUserRequest extends FormRequest
{
    public function authorize()
    {
        return true;
    }

    public function rules(): array
    {
        $userId = $this->user->id;

        $rules = [
            'name' => 'required|string|max:255',
            'email' => [
                'required',
                'string',
                'email',
                'max:255',
                Rule::unique('users')->ignore($userId)->whereNull('deleted_at'),
            ],
            'image' => [
                'image',
                'max:1024',
                'mimes:jpg,png',
            ],
            'is_admin' => 'required|string|max:10',
        ];

        if (!empty($this->input('password'))) {
            $rules['password'] = ['sometimes', 'required', 'string', 'min:8', 'regex:/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[@$!%*?&])/', 'confirmed'];
        }

        return $rules;
    }

    public function messages(): array
    {
        return [
            'password.regex' => 'パスワードは少なくとも1つの半角英字、数字、および記号(@$!%*?&)を含む必要があります。',
        ];
    }
}

sometimes

フィールドが存在する場合にのみ、そのフィールドにバリデーションルールを適用するために使用されます。
https://laravel.com/docs/11.x/validation#complex-conditional-array-validation

confirmed

指定されたフィールドがその名前のフィールドの確認用であることを示します。

今回の場合では、passwordフィールドの値がconfirmedルールによってpassword_confirmationフィールド(ビューの修正で確認してください)と一致することがバリデーションになっています。
https://laravel.com/docs/11.x/validation#validating-passwords

認可を定義する

認可はAuthServiceProviderというサービスプロバイダー内に記述します。これは、アプリケーションの認証と認可の設定を集中管理するための場所です。

bootメソッドで定義した認可ロジックは、アプリケーション全体で利用可能になります。

app/Providers/AppServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;

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

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        Gate::define('admin', function ($user) {
            return $user->is_admin === '1'; // is_adminが '1' の場合に true を返す
        });

        Gate::define('edit_update', function ($user, $authUser, $editingUser) {
            return $authUser->is_admin === '1' || $authUser->id === $editingUser->id;
        });
    }
}

Gate

app/Providers/AppServiceProvider.phpに記載しているGateは必ず第一引数にGateは必ず第一引数にログインしているユーザーインスタンスを受け取っています。

下記のように第二引数までしか記載していない場合でも

app/Http/Controllers/UsersController.php
Gate::authorize('show_edit_update', [auth()->user()]);

AppServiceProvider.phpでは下記のように受け取ることができます。

app/Providers/AppServiceProvider.php
 public function boot(): void
    {
       Gate::define('show_edit_update', function ($user, $authUser, $editingUser) {
            return $authUser->is_admin === '1' || $authUser->id === $editingUser->id;
        });
    }

https://laravel.com/docs/11.x/authorization#gates

コントローラーの修正

各メソッドに認可などを追加してください。

app/Http/Controllers/UsersController.php
<?php

namespace App\Http\Controllers;

use App\Http\Requests\User\UpdateUserRequest;
use App\Http\Requests\User\UserRequest;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;

class UsersController
{
    /**
     * Display a listing of the resource.
     */
    public function index()
    {
        $authUser = auth()->user();
        if (Gate::allows('admin', $authUser)) {
            $users = User::paginate(5); // 管理者はすべてのユーザーを5件ごとに表示
        } else {
            $users = User::where('id', $authUser->id)->get();; // 管理者でない場合は自分の情報のみ表示なのでページングは不要
        }
        return view('users.index', compact('users', 'authUser'));
    }

    /**
     * Show the form for creating a new resource.
     */
    public function create()
    {
        Gate::authorize('admin');
        return view('users.create'); // ここで 'create.blade.php' ビューを返します
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(UserRequest $request)
    {
        Gate::authorize('admin');
        DB::beginTransaction();
        try {
            $user = new User();

            // 投稿フォームから送信されたデータを取得し、インスタンスの属性に代入する
            $user->name = $request->input('name');
            $user->email = $request->input('email');
            $user->password = $request->input('password');
            $user->is_admin = $request->input('is_admin', '0'); // is_adminに初期値として'0'を代入

            $user->save();

            // 画像がアップロードされているか確認し、保存
            if ($request->hasFile('image')) {
                $file = $request->file('image');
                $filename = date('Ymd_His') . '_user_id_' . $user->id . '.' . $file->getClientOriginalExtension(); // タイムスタンプをファイル名に使用
                $file->storeAs('public/images', $filename); // imagesディレクトリにファイルを保存
                $user->image = $filename;
                $user->save();
            }

            DB::commit();

            return view('users.create', compact('user'));
        } catch (\Exception $e) {
            DB::rollBack();
            return back()->with('message', '登録に失敗しました。' . $e->getMessage());
        }
    }

    /**
     * Display the specified resource.
     */
    public function show(User $user)
    {
        $authUser = auth()->user();
        if (Gate::allows('admin', $authUser) || auth()->user()->id === $user->id) {
            return view('users.show', compact('user'));
        }

        // それ以外の場合は403権限なしを表示する
        abort(403, 'Forbidden');
    }
    /**
     * Show the form for editing the specified resource.
     */
    public function edit(User $user)
    {
        Gate::authorize('edit_update', [auth()->user(), $user]);

        return view('users.edit', compact('user'));
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(UpdateUserRequest $request, User $user)
    {
        Gate::authorize('edit_update', [auth()->user(), $user]);
        $data = $request->validated();

        $passwordChanged = false;
        if (!empty($data['password'])) {
            $passwordChanged = true;
        } else {
            unset($data['password']); // パスワードフィールドをデータ配列から削除
        }

        // 画像を更新する前に旧画像のファイル名を保持
        $oldImage = $user->image;

        if ($request->hasFile('image')) {
            $file = $request->file('image');
            $filename = date('Ymd_His') . '_user_id_' . $user->id . '.' . $file->getClientOriginalExtension(); // タイムスタンプをファイル名に使用
            $file->storeAs('public/images', $filename); // imagesディレクトリにファイルを保存
            $data['image'] = $filename; // DBにはファイル名のみを保存
        }

        DB::beginTransaction();
        try {
            $user->update($data);

            // 新しい画像がアップロードされていれば、古い画像を削除
            if ($request->hasFile('image') && $oldImage) {
                Storage::disk('public')->delete('images/' . $oldImage);
            }

            DB::commit();
            return redirect()->route('users.edit', $user->id)->with(compact('user', 'passwordChanged'));
        } catch (\Exception $e) {
            DB::rollback();
            return back()->with(['message' => '更新中にエラーが発生しました。' . $e->getMessage()]);
        }
    }

    public function destroy(User $user)
    {
        Gate::authorize('admin');
        DB::beginTransaction();
        try {
            $oldImage = $user->image;

            $user->delete();  // ユーザー情報をデータベースから削除

            // 画像が存在する場合、削除する
            if ($oldImage && Storage::disk('local')->exists('public/images/' . $oldImage)) {
                Storage::disk('local')->delete('public/images/' . $oldImage);
            }

            DB::commit();

            return redirect('/users')->with('message', '削除が完了しました!');
        } catch (\Exception $e) {
            DB::rollBack();
            return back()->with(['message' => '削除に失敗しました。' . $e->getMessage()]);
        }
    }
}

paginate

https://laravel.com/docs/11.x/eloquent-resources#pagination

Gate::authorize

指定したゲートが許可されない場合に\Illuminate\Auth\Access\AuthorizationExceptionを自動的に投げます。この例外は通常、Laravelがハンドルして適切なHTTP応答(デフォルトで403 Forbidden)を返します。

getClientOriginalExtensionメソッド

アップロードされたファイルの元の拡張子を取得します。

date('Ymd_His')

現在の日付と時刻を年月日時分秒で表します。
今回はこの日付にユーザーのIDと拡張子を組み合わせることで、ファイル名を20240101_120000_user_id_1.jpgのようにして他のアップロードと重複しないようにします。

storeAs

ファイルを指定されたディレクトリに保存します。
第1引数には保存先のディレクトリを指定し、第2引数には保存後のファイル名を指定します。
https://laravel.com/docs/11.x/filesystem#specifying-a-disk

403エラーページの作成

現在のままでは、認可がない操作を行った場合に403エラーが表示されてしまいます。

下記を参考にエラーページを作成してください。
https://zenn.dev/nenenemo/articles/b9da36f99b58b2#エラーページの作成

シンボリックリンクの作成

シンボリックリンクとは

シンボリックリンクは、ファイルやディレクトリのショートカットとして機能し、本来のアクセスとは異なる代理のフォルダからファイルやフォルダを参照することができます。

Laravelアプリケーションでは、storage/app/publicディレクトリ(ユーザーがアップロードした画像などを保存するディレクトリ)に保存されたファイルをWebサーバーから直接アクセスを可能にするためには、公開ディレクトリであるpublicにシンボリックリンクを作成する必要があります。

シンボリックリンクを作成してください。

sail artisan storage:link

シンボリックリンクが作成されたか確認してください。

ls -la public

下記の表示が含まれていれば問題ないです。

storage -> /var/www/html/storage/app/public

またはpublicで下記のように表示されていると思います。これは削除しないようにしてください。

今回はDockerコンテナを使用しているので、下記のようになっている場合はpublic/storageを削除してシンボリックリンクを再作成してください。

storage -> /Users/<ユーザー名>/Desktop/<プロジェクト名>/storage/app/public

public/storageを削除するコマンドです。

rm public/storage

削除せずにシンボリックリンクを再作成しようとすると、次のように表示されます。

ビューの修正

一覧

resources/views/users/index.blade.php
@extends('layouts.app')

@section('title', 'ユーザー一覧')

@section('content')
    <h1 class="text-2xl font-bold mb-4">ユーザー一覧</h1>

    <!-- ユーザー登録ページへのリンク -->
    <a href="{{ route('users.create') }}"
        class="mb-4 inline-block bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
        ユーザー登録
    </a>

    <!-- セッションメッセージ -->
    @if (session('message'))
        <div><strong>{{ session('message') }}</strong></div>
    @endif

    <div class="overflow-x-auto">
        <table class="min-w-full divide-y divide-gray-200">
            <thead>
                <tr>
                    <th class="px-6 py-3 bg-gray-50 text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
                    <th class="px-6 py-3 bg-gray-50 text-xs font-medium text-gray-500 uppercase tracking-wider">名前</th>
                    <th class="px-6 py-3 bg-gray-50 text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
                </tr>
            </thead>
            <tbody class="bg-white divide-y divide-gray-200">
                @if (isset($users))
                    @foreach ($users as $user)
                        <tr>
                            <td class="px-6 py-4 whitespace-nowrap">{{ $user->id }}</td>
                            <td class="px-6 py-4 whitespace-nowrap"><a href="{{ route('users.show', $user->id) }}"
                                    class="text-blue-500 hover:underline">{{ $user->name }}</a></td>
                            <td class="px-6 py-4 whitespace-nowrap">
                                <a href="{{ route('users.edit', $user->id) }}"
                                    class="text-indigo-600 hover:text-indigo-900">編集</a>
                                @can('admin', $authUser)
                                    <form action="{{ route('users.destroy', $user->id) }}" method="POST" class="inline">
                                        @csrf
                                        @method('DELETE')
                                        <button type="submit" class="text-red-600 hover:text-red-900 ml-20"
                                            onclick="return confirm('本当に削除しますか?')">削除</button>
                                    </form>
                                @endcan
                            </td>
                        </tr>
                    @endforeach
                @endif
            </tbody>
        </table>

        <!-- ページネーションリンク -->
        <div class="mt-4">
            {{ $users->links() }}
        </div>
    </div>
@endsection 

@can

ユーザーに与えられた権限が特定のアクションを実行する権限があるかどうかをチェックするために使われます。
@can('admin', $authUser)adminapp/Providers/AppServiceProvider.phpで定義された認可です。
https://laravel.com/docs/11.x/authorization#gates-supplying-additional-context

@cannot

@canの逆の動作をするディレクティブとして@cannotもあります。
@cannotは指定された権限がユーザーにない場合にコンテンツを表示するために使用します。

登録

resources/views/users/create.blade.php
@extends('layouts.app')

@section('title', '新規作成')

@section('content')
    <form method="POST" action="{{ route('users.store') }}" enctype="multipart/form-data">
        @csrf
        <div>
            <h1 class='text-center font-bold '>新規作成</h1>
            <!-- 名前フィールド -->
            <div class="mt-4">
                <label for="name">名前</label>
                <input id="name" type="text"
                    class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
                    name="name" value="{{ old('name') }}" required placeholder="">
                @error('name')
                    <div class="text-red-500">{{ $message }}</div>
                @enderror
            </div>

            <!-- メールアドレスフィールド -->
            <div class="mt-4">
                <label for="email">メールアドレス</label>
                <input id="email" type="email"
                    class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
                    name="email" value="{{ old('email') }}" required placeholder="">
                @error('email')
                    <div class="text-red-500">{{ $message }}</div>
                @enderror
            </div>

            <!-- パスワードフィールド -->
            <div class="mt-4">
                <label for="password">パスワード</label>
                <input id="password" type="password"
                    class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
                    name="password" required placeholder="">
                @error('password')
                    <div class="text-red-500">{{ $message }}</div>
                @enderror
            </div>

            <!-- パスワード確認フィールド -->
            <div class="mt-4">
                <label for="password_confirmation">パスワード確認</label>
                <input id="password_confirmation" type="password"
                    class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
                    name="password_confirmation" required placeholder="">
            </div>

            <!-- 画像アップロードフィールド -->
            <div class="mt-4">
                <label for="image">ユーザーアイコン(jpg / png の形式のみで1MB以内)</label>
                <input id="image" type="file" class="block" name="image" accept="image/png,image/jpeg">
                @error('image')
                    <div class="text-red-500">{{ $message }}</div>
                @enderror
            </div>

            <!-- 登録ボタン -->
            <div class="flex items-center justify-center my-4">
                <button type="submit">
                    登録を完了する
                </button>
            </div>

            <!-- セッションメッセージ -->
            @if (session('message'))
                <div><strong>{{ session('message') }}</strong></div>
            @endif

            <!-- ユーザー情報 -->
            @isset($user)
                <strong>ユーザー登録が完了しました。</strong>
                <h2>登録したユーザーの情報</h2>
                <p>名前: {{ $user->name }}</p>
                <p>メールアドレス: {{ $user->email }}</p>
                <p>パスワード: ********</p>
                @if ($user->image)
                    <img src="{{ asset('storage/images/' . $user->image) }}" alt="現在のアイコン" class="mx-auto h-[300px]">
                @endif
            @endisset
        </div>

        @if ($errors->any())
            <div class="alert alert-danger">
                <ul>
                    @foreach ($errors->all() as $error)
                        <li>{{ $error }}</li>
                    @endforeach
                </ul>
            </div>
        @endif

        <div class="mt-4">
            <a href="{{ route('users.index') }}" class="text-blue-500 hover:underline">ユーザー一覧に戻る</a>
        </div>
    </form>
@endsection

詳細表示

resources/views/users/show.blade.php
@extends('layouts.app')

@section('title', 'ユーザー情報')

@section('content')
    <div class="container mx-auto">
        @isset($user)
            <h1 class="text-2xl font-semibold mb-4">{{ $user->name }} さんの情報</h1>
            <table class="min-w-full border-collapse border border-gray-200">
                <tr>
                    <td class="border border-gray-200 px-4 py-2 font-semibold">ユーザーアイコン</td>
                    <td class="border border-gray-200 px-4 py-2">
                        @if ($user->image)
                            @if ($user->image)
                                <img src="{{ asset('storage/images/' . $user->image) }}" alt="現在のアイコン" class="mx-auto h-[300px]">
                            @endif
                        @else
                            ユーザーアイコンは設定されていません。
                        @endif
                    </td>
                </tr>
                <tr>
                    <td class="border border-gray-200 px-4 py-2 font-semibold">ID</td>
                    <td class="border border-gray-200 px-4 py-2">{{ $user->id }}</td>
                </tr>
                <tr>
                    <td class="border border-gray-200 px-4 py-2 font-semibold">名前</td>
                    <td class="border border-gray-200 px-4 py-2">{{ $user->name }}</td>
                </tr>
                <tr>
                    <td class="border border-gray-200 px-4 py-2 font-semibold">メールアドレス</td>
                    <td class="border border-gray-200 px-4 py-2">{{ $user->email }}</td>
                </tr>

                <tr>
                    <td class="border border-gray-200 px-4 py-2 font-semibold">管理者権限</td>
                    <td class="border border-gray-200 px-4 py-2">{{ $user->is_admin ? 'はい' : 'いいえ' }}</td>
                </tr>
            </table>
        @endisset

        <div class="mt-4">
            <a href="{{ route('users.index') }}" class="text-blue-500 hover:underline">ユーザー一覧に戻る</a>
        </div>
    </div>
@endsection

更新

resources/views/users/edit.blade.php
@extends('layouts.app')

@section('title', 'ユーザー情報編集')

@section('content')
    <form method="POST" action="{{ route('users.update', $user->id) }}" enctype="multipart/form-data">
        @csrf
        @method('PUT')
        <div>
            <h1 class='text-center font-bold '>ユーザー情報編集(変更する箇所のみ入力してください)</h1>

            <!-- 名前フィールド -->
            <div class="mt-4">
                <label for="name">名前</label>
                <input id="name" type="text"
                    class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
                    name="name" value="{{ old('name', $user->name) }}" required placeholder="">
                @error('name')
                    <div class="text-red-500">{{ $message }}</div>
                @enderror
            </div>

            <!-- メールアドレスフィールド -->
            <div class="mt-4">
                <label for="email">メールアドレス</label>
                <input id="email" type="email"
                    class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
                    name="email" value="{{ old('email', $user->email) }}" required placeholder="">
                @error('email')
                    <div class="text-red-500">{{ $message }}</div>
                @enderror
            </div>

            <!-- パスワードフィールド -->
            <div class="mt-4">
                <label for="password">新しいパスワード</label>
                <input id="password" type="password"
                    class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
                    name="password" placeholder="">
                @error('password')
                    <div class="text-red-500">{{ $message }}</div>
                @enderror
            </div>

            <!-- パスワード確認フィールド -->
            <div class="mt-4">
                <label for="password_confirmation">新しいパスワード確認</label>
                <input id="password_confirmation" type="password"
                    class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
                    name="password_confirmation" placeholder="">
            </div>

            <!-- 画像アップロードフィールド -->
            <div class="mt-4">
                <label for="image">ユーザーアイコン(jpg / png の形式のみで1MB以内)</label>
                <input id="image" type="file" class="block" name="image" accept="image/png,image/jpeg">
                @error('image')
                    <div class="text-red-500">{{ $message }}</div>
                @enderror
                <!-- 現在のアイコン表示 -->
                @if ($user->image)
                    <p>現在のアイコン</p>
                    <img src="{{ asset('storage/images/' . $user->image) }}" alt="現在のアイコン" class="mx-auto h-[300px]">
                @endif
            </div>

            <!-- 管理者権限フィールド -->
            <div class="mt-4">
                <label for="is_admin">管理者権限</label>
                @if ($user->is_admin == '1')
                    <p>現在は権限あり</p>
                @else
                    <p>現在は権限なし</p>
                @endif
                <div>
                    <input type="radio" id="admin_yes" name="is_admin" value="1"
                        {{ $user->is_admin == '1' ? 'checked' : '' }}>
                    <label for="admin_yes">あり</label>

                    <input type="radio" id="admin_no" name="is_admin" value="0"
                        {{ $user->is_admin != '1' ? 'checked' : '' }}>
                    <label for="admin_no">なし</label>
                </div>
            </div>

            <!-- セッションメッセージ -->
            @if (session('message'))
                <div><strong>{{ session('message') }}</strong></div>
            @endif

            <!-- ユーザー情報の表示 -->
            @if (session('user'))
                <div>
                    <strong>ユーザー情報が更新されました。</strong>
                    <h2>更新したユーザーの情報</h2>
                    <p>名前: {{ session('user')->name }}</p>
                    <p>メールアドレス: {{ session('user')->email }}</p>
                    @if (session('passwordChanged'))
                        <p>パスワード: ********</p>
                    @endif
                    @if (session('user')->is_admin === '1')
                        <p>管理者権限: あり</p>
                    @else
                        <p>管理者権限: なし</p>
                    @endif

                    @if (session('user')->image)
                        @if ($user->image)
                            <img src="{{ asset('storage/images/' . $user->image) }}" alt="現在のアイコン"
                                class="mx-auto h-[300px]">
                        @endif
                    @endif
                </div>
            @endif

            <!-- 更新ボタン -->
            <div class="flex items-center justify-center my-4">
                <button type="submit" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
                    更新を完了する
                </button>
            </div>

            <div class="mt-4">
                <a href="{{ route('users.index') }}" class="text-blue-500 hover:underline">ユーザー一覧に戻る</a>
            </div>
        </div>
    </form>
@endsection

SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo for mysql failed: nodename nor servname provided, or not known

コンテナ内でマイギュレーションを実行しているか確認してください。

php artisan migrate

Serialization of 'Illuminate\Http\UploadedFile' is not allowed

セッションにファイルインスタンスを保存しようとしたときに発生します。Laravelのセッションは、データをシリアライズして保存するため、UploadedFileのようなオブジェクトは直接保存できません。

SQLSTATE[HY000]: General error: 1364 Field '<カラム名>' doesn't have a default value

データベースに挿入される際にリクエストクラスでnullableと定義していても、デフォルト値が設定されていないと、このエラーが発生します。

値がnullの場合でもデータベースに明示的にnullを代入してください。

終わりに

何かありましたらお気軽にコメント等いただけると助かります。
ここまでお読みいただきありがとうございます🎉

Discussion