3️⃣

【Laravel】CRUD機能を作成する③更新機能(Update)

2024/05/29に公開

はじめに

下記の続きになります。
https://zenn.dev/nenenemo/articles/b9da36f99b58b2

ユーザーの一覧から編集を押すと編集画面表示され、データを更新できるようにしたいと思います。

編集ボタンの追加

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">
                @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>
                        </td>
                    </tr>
                @endforeach
            </tbody>
        </table>
    </div>
@endsection

編集画面の追加

resources/views/users/edit.blade.phpを作成してください。

php artisan make:view users/edit

パスワードは①の登録機能の場合と異なり、入力された場合のみ変更するようにしています。

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

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

@section('content')
    <form method="POST" action="{{ route('users.update', $user->id) }}">
        @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>

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

            <!-- ユーザー情報の表示 -->
            @if (session('user'))
                <div>
                    <h2>登録したユーザーの情報</h2>
                    <p>名前: {{ session('user')->name }}</p>
                    <p>メールアドレス: {{ session('user')->email }}</p>
                    @if (session('passwordChanged'))
                        <p>パスワード: ********</p>
                    @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

ルート設定

web.php
<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\UsersController;

Route::prefix('users')->group(function () {
    Route::get('/', [UsersController::class, 'index'])->name('users.index');
    Route::get('/create', [UsersController::class, 'create'])->name('users.create');
    Route::post('/store', [UsersController::class, 'store'])->name('users.store');
    Route::get('/{user}', [UsersController::class, 'show'])->name('users.show');
    Route::get('/{user}/edit', [UsersController::class, 'edit'])->name('users.edit');
    Route::put('/{user}', [UsersController::class, 'update'])->name('users.update');
});

ルートモデルバインディング

②の読み込み機能ではRoute::get('/{id}', [UsersController::class, 'show'])->name('users.show');と記述していたものの{id}{user}に変更しています。

これはルートモデルバインディングを使用しており、コントローラーのメソッドの引数としてモデルのインスタンスを直接受け取ることができます。

先ほどのルート設定では、{user}というパラメーターがUserモデルのインスタンスと紐付いています。一致するモデルのインスタンスがデータベースで見つからない場合、404 HTTP レスポンスが自動的に生成されます。

次に、UsersControllershowメソッドを次のように変更します。

    public function show(User $user)
    {
        return view('users.edit', compact('user'));
    }

このようにすることで、editメソッドの引数としてUserモデルのインスタンスが自動的に注入されます。これにより、コントローラー内で明示的にデータを取得する必要がなくなり、より簡潔なコードを書くことができます。

ちなみに、ルートモデルバインディングもfindと同様に対象のモデルを検索する際に、ユーザーからの入力値を直接クエリに組み込むのではなく、プレースホルダーを使用して安全に検索を行っているため、SQLインジェクション対策が行われています。
https://laravel.com/docs/11.x/routing#route-model-binding

id以外のカラムを基準にするには、下記のようにルート設定を行ってください。

web.php
Route::get('/{user:email}', [UsersController::class, 'show'])->name('users.show');

または、getRouteKeyNameメソッドを使用することで基準を指定できます。

app/Models/User.php
<?php

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use HasFactory, Notifiable;

    /**
     * Get the route key for the model.
     *
     * @return string
     */
    public function getRouteKeyName()
    {
        return 'email';
    }

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

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * Get the attributes that should be cast.
     *
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password' => 'hashed', // パスワードをハッシュ化
        ];
    }
}

getRouteKeyNameメソッドを使用した場合には、ルート設定は以下のままで問題ないです。

web.php
Route::get('/{user}', [UsersController::class, 'show'])->name('users.show');

https://laravel.com/docs/11.x/routing#customizing-the-default-key-name

リクエストクラスの作成

①で登録用のリクエストは作成しましたが、更新した場合のリクエストはまだないので作成します。

php artisan make:request UpdateUserRequest
app/Http/Requests/UpdateUserRequest.php
<?php

namespace App\Http\Requests;

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), // 重複チェックで自分のメールアドレスを除外
            ],
        ];

        // パスワードが入力されている場合にのみ、パスワードのバリデーションルールを適用
        if (!empty($this->input('password'))) {
            $rules['password'] = ['string', 'min:8', 'regex:/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[@$!%*?&])/',];
        }

        return $rules;
    }

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

Rule::

Laravelのバリデーションルールを定義するためのクラスです。

バリデーションルールが複雑になった場合は、make:ruleArtisanコマンドを使用してバリデーションルールを作成した方が良いと思います。
https://readouble.com/laravel/11.x/ja/validation.html

FormRequest

$this->input('password')を使用して入力情報を取得できるのは、FormRequestクラスがリクエストに含まれるデータを取得するための便利なメソッドを提供しているからです
https://laravel.com/docs/11.x/validation#form-request-validation

コントローラー設定

updateメソッドを作成してください。

app/Http/Controllers/UsersController
  public function update(UpdateUserRequest $request, User $user)
    {
        $data = $request->validated();

        $passwordChanged = false; // パスワードが変更されたかどうかのフラグ

        // パスワードが入力されている場合のみ更新
        if (!empty($data['password'])) {
            $passwordChanged = true;
        } else {
            unset($data['password']); // パスワードフィールドをデータ配列から削除
        }

        DB::beginTransaction();

        try {
            $user->update($data);

            DB::commit();

            return
                redirect()->route('users.edit', $user->id)->with(compact('user', 'passwordChanged'));;
        } catch (\Exception $e) {
            DB::rollback();

            return back()->with(['message' => '更新中にエラーが発生しました。' . $e->getMessage()]);
        }
    }

次はCRUDの④削除機能(Delete)についての記事を書きたいと思います!

終わりに

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

Discussion