Laravel を使って簡単なCRUD機能を実装 ①

# Laravel を使って簡単なCRUD機能を実装（フォームの作成編）
![スクリーンショット 2025-03-14 23.01.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4034432/df44354e-fc23-4652-b16b-079b26a0537c.png)
このようなフォームを作成することを目指す。

## 1. モデルとマイグレーションファイルの作成
まずデータベース部分すなわちモデルとマイグレーションファイルを作成していく。
次のコードを実行し、マイグレーションファイルを作成する。
```bash
sail artisan make:model Post -m
```
そして以下のコードを書く

```（日付）_create_posts_table.php

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

public function up(): void
{
    Schema::create('posts', function (Blueprint $table) {
        $table->id(); // 自動増分IDを作成
        $table->string('title'); // 'title' という名前のカラム（文字列型）
        $table->text('body'); // 'body' という名前のカラム（テキスト型）
        $table->text('image'); // 'image' という名前のカラム（テキスト型）
        $table->timestamps(); // 作成日時と更新日時を記録するカラム（created_at, updated_at）
    });
}
```
### 以下説明
```php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
```

• **Migration**: マイグレーションの基底クラスです。これを使って、データベースのテーブルを作成したり削除したりします。 
•  **Blueprint**: テーブルを構築するためのクラスで、どんなカラム（列）を持つテーブルを作るかを定義します。 
•  **Schema**: LaravelのDBスキーマビルダーです。テーブルの作成や変更を行うために使います。


```php
public function up(): void
{
    Schema::create('posts', function (Blueprint $table) {
        $table->id(); // 自動増分IDを作成
        $table->string('title'); // 'title' という名前のカラム（文字列型）
        $table->text('body'); // 'body' という名前のカラム（テキスト型）
        $table->text('image'); // 'image' という名前のカラム（テキスト型）
        $table->timestamps(); // 作成日時と更新日時を記録するカラム（created_at, updated_at）
    });
}
```

•  **Schema::create('posts', ...)**：posts という名前のテーブルを作成します。 
•  **$table->id()**：自動増分される主キーのIDカラムを追加します。 
•  **$table->string('title')**：title という文字列型のカラムを追加します。 
•  **$table->text('body')**：body というテキスト型のカラムを追加します。 
•  **$table->text('image')**：image というテキスト型のカラムを追加します（例えば、画像のURLなどを格納する場合などに使います）。 
•  **$table->timestamps()**：created_at（作成日時）と updated_at（更新日時）のカラムを自動的に追加します。

```php
public function down(): void
{
    Schema::dropIfExists('posts');
}
```

**Schema::dropIfExists('posts')**：もし posts テーブルが存在していれば、それを削除します。マイグレーションのロールバック（変更を元に戻す）を行うために使います。

## 2. ビューの作成
続いてResources/views/postの create.blade.php に以下のコードを書く

```php
<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
           フォームですよ
        </h2>
    </x-slot>

    <div class="max-w-7xl mx-auto px-6">
        @if(session('message'))
            <div class="text-red-600 font-bold">
                {{session('message')}}
            </div>
        @endif
        <form method="post" action="{{ route('post.store') }}" enctype="multipart/form-data">
            @csrf

            <div class="mt-8">
                <div class="w-full flex flex-col">
                    <label for="title" class="font-semibold mt-4">件名</label>
                    <x-input-error :messages="$errors->get('title')" class="mt-2" />
                    <input type="text" name="title" class="w-full py-2 px-3 border border-gray-300 rounded-md" id="title" placeholder="件名を入力してください"
                    value="{{old('title')}}">
                </div>
            </div>

            <div class="w-full flex flex-col">
                <label for="body" class="font-semibold mt-4">本文</label>
                <x-input-error :messages="$errors->get('body')"class="mt-2"/>
                <textarea name="body" class="w-full py-2 px-3 border border-gray-300 rounded-md" id="body" cols="30" rows="5" placeholder="本文を入力してください">
                    {{old('body')}}
                </textarea>
            </div>

            <label for="image">画像:</label>
            <input id="image" type="file" name="image">

            <x-primary-button type="submit" class="mt-4">
                送信する
            </x-primary-button>
        </form>
    </div>
</x-app-layout>
```

### 以下説明

```php
<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
            フォームですよ
        </h2>
    </x-slot>
```

•	**x-app-layout** は、Laravelのコンポーネントを利用してページ全体のレイアウトを定義します。
•	**x-slot name="header"** は、ページのヘッダー部分に表示する内容を定義しています。ここでは「フォームですよ」という見出しが表示されます。

```
@if(session('message'))
    <div class="text-red-600 font-bold">
        {{ session('message') }}
    </div>
@endif
```

•  **session('message')** で、セッションに保存されたメッセージ（例えば、フォーム送信後の成功メッセージ）を表示しています。 
•  メッセージがあれば、赤い文字でメッセージが表示されます。

```php
<form method="post" action="{{ route('post.store') }}" enctype="multipart/form-data">
    @csrf
```

•  **method="post"** はフォーム送信のHTTPメソッドとしてPOSTを使用します。 

•  **action="{{ route('post.store') }}"** は、フォームの送信先を指定します。ここでは post.store という名前のルートに送信されます（コントローラーの store メソッド）。 
•  **@csrf** はCSRF（クロスサイトリクエストフォージェリ）保護用のトークンを生成します。

**method="post"の説明**
method="post"は、HTMLのフォームタグで使用する属性で、フォームが送信される際に使用するHTTPメソッドを指定します。


**HTTPメソッド**
HTTPメソッドにはいくつかの種類がありますが、主に使うのは以下の2つです：

•	GET: データを取得するためのリクエストです。フォームを送信する際、データはURLの一部として送信されます（URLの後ろにクエリパラメータとして追加されます）。

•	POST: データをサーバーに送信するためのリクエストです。フォームを送信する際、データはリクエストの本文に含まれ、URLに表示されません。


**method="post"が使われる理由**
method="post"を使うと、ユーザーが入力したデータ（タイトル、本文、画像など）が安全にサーバーに送信されます。例えば、ユーザーが記事を投稿する場合、その内容をサーバーに送信し、データベースに保存したり、処理を行ったりします。



•	POSTメソッドは、データを送信する際にURLに表示されず、リクエストの「ボディ」にデータが含まれるため、GETメソッドよりもセキュアです。

•	また、ファイルアップロード（画像のような）を行う場合には、method="post"とenctype="multipart/form-data"を指定しなければなりません。

```php
<div class="mt-8">
    <div class="w-full flex flex-col">
        <label for="title" class="font-semibold mt-4">件名</label>
        <x-input-error :messages="$errors->get('title')" class="mt-2" />
        <input type="text" name="title" class="w-full py-2 px-3 border border-gray-300 rounded-md" id="title" placeholder="件名を入力してください"
            value="{{ old('title') }}">
    </div>
</div>
```


•  **label for="title"**:
•	この<label>タグは、フォームの「件名」フィールドにラベルを付けるために使います。**for="title"**は、このラベルがどのフォームフィールドに対応しているかを指定しており、titleという名前の入力フィールドに関連づけられます。

•	つまり、このラベルをクリックすると、タイトルの入力フィールドにフォーカスが移ります。

•**x-input-error :messages="$errors->get('title')**:

•	これはLaravelのカスタムコンポーネントで、入力フィールドにエラーがあった場合にエラーメッセージを表示するためのものです。

•**errors->get('title')** は、もし「件名」フィールドにバリデーションエラーがあった場合、そのエラーメッセージを取得して表示します。

•  **input type="text" name="title" ... value="{{ old('title') }}">**:

•**<input** タグは、実際のユーザー入力を受け取るためのテキストフィールドです。

•	**name="title"** は、このフィールドが送信されるデータの中で「title」という名前で値を送信することを意味します。

•	**value="{{ old('title') }}"** は、フォームを送信した後、エラーがあった場合でも、前回入力された「タイトル」の値を再表示するためのものです。これにより、ユーザーは再度入力する手間を省けます。

## Controllerの作成
app/Http/Controllers/PostControllerを作成していく、なお上ではcreate部分のことしか話していないが、ここではCRUD全てのことについて触れる。
このコードは、Laravelで作成されたPostControllerというコントローラーの内容です。主に「投稿」に関連する操作（CRUD）を扱っており、Postモデルに基づいてデータを操作します。以下に、各メソッドの詳細な説明を行います。


```php
<?php

namespace App\Http\Controllers;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
use Illuminate\Http\Request;
use App\Models\Post;


class PostController extends Controller
{
    public function index(){
        //postsデータ
        $posts=Post::all();
        return view('post.index', compact('posts'));
    }
    public function create()
    {
        return view('post.create');
    }

    public function store(Request $request){
        Gate::authorize('test');
        $validated =  $request->validate([
            'title'=>'required|max:20',
            'body'=>'required|max:400',
            'image' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048'// 画像バリデーション
            
        ]);
        $validated['user_id']=auth()->id();

        if ($request->hasFile('image')) {
            $validated['image'] = $request->file('image')->store('images', 'public'); // `storage/app/public/images` に保存
        }

        $post = Post::create($validated);
        $request->session()->flash('message','保存しました');
        return back();
    }
    
    //タイプヒント
    public function show(Post $post){
        return view('post.show',compact('post'));
    }

    public function edit(Post $post){
        return view('post.edit',compact('post'));
    }


    public function update(Request $request, Post $post){
        $validated =  $request->validate([
            'title'=>'required|max:20',
            'body'=>'required|max:400',
            'image' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048'// 画像バリデーション
        ]);

        $validated['user_id']=auth()->id();

        if ($request->hasFile('image')) {
            // 既存の画像を削除
            if ($post->image) {
                Storage::disk('public')->delete($post->image);
            }
            // 新しい画像を保存
            $validated['image'] = $request->file('image')->store('images', 'public');
        }

        $post->update($validated);

        $request->session()->flash('message','更新しました');
        return back();
    }

    public function destroy(Request $request,Post $post){

        if ($post->image) {
            Storage::disk('public')->delete($post->image);
        }

        $post->delete();
        //sessionは一時保存
        $request->session()->flash('message','削除しました');
        return redirect()->route('post.index');
    }
    
}
```

### 以下説明

```php
public function index(){
    $posts = Post::all();
    return view('post.index', compact('posts'));
}
```

•  **目的**: すべての投稿を表示するためのメソッド。 
•  **処理内容**: Post::all()で、postsテーブルの全データを取得します。それをpost.indexビューに渡して、表示します。

```php
public function store(Request $request){
    Gate::authorize('test');
    $validated =  $request->validate([
        'title'=>'required|max:20',
        'body'=>'required|max:400',
        'image' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048'
    ]);
    $validated['user_id'] = auth()->id();
    
 if ($request->hasFile('image')) {
        $validated['image'] = $request->file('image')->store('images', 'public');
    }

    $post = Post::create($validated);
    $request->session()->flash('message', '保存しました');
    return back();
}
```


•	**目的** : 新しい投稿をデータベースに保存するためのメソッド。
•	**処理内容** : 
1.	**Gate::authorize('test')**: この行は、ユーザーがtestという権限を持っているか確認します。権限に応じてアクセスを制限できます。
（Gateに関しては別記事で紹介）
2.	**$request->validate([...])** : バリデーションを行い、title（最大20文字）、body（最大400文字）、image（画像ファイルとして最大2MB）などを検証します。

（Requestは、HTTPリクエストに関する情報を管理するクラスです。$requestは、ユーザーがフォームを送信したときや、APIリクエストがあったときに、そのリクエストに関するデータを取得するために使われます。
例えば、ユーザーがフォームに入力したデータは、$requestを通じてアクセスできます。これには、フォームのデータ（title, body, 画像など）や、HTTPヘッダ、クッキー、セッションなど、リクエストに関連するすべての情報が含まれます。

**Request $request**: ユーザーから送信されたHTTPリクエストに関連するデータを取得するためのオブジェクトです。これを使ってフォームデータを操作します。）


（validatedは、バリデーション（入力検証）を通過したデータを格納するための変数です。具体的には、**$request->validate()** メソッドを使ってフォームのデータを検証した後、その検証を通過したデータがvalidatedに格納されます。
バリデーションは、ユーザーがフォームに入力したデータが正しいかどうかを確認するための処理です。例えば、titleが必須であり、最大文字数が20であることを確認したり、imageが画像ファイルであることを確認するなどです。
**$request->validate()** は、バリデーションを実行し、もしバリデーションが失敗した場合、自動的にエラーメッセージを返し、処理が中断されます。バリデーションに成功した場合、リクエストデータをvalidatedに格納し、それを後続の処理（データベース保存など）に使用します。）


3.	**$validated['user_id'] = auth()->id();** : 現在ログインしているユーザーのIDを投稿に関連づけます。
4.	**$request->hasFile('image')** : もし画像ファイルがアップロードされていれば、storage/app/public/imagesに保存します。
5.	最後に、**Post::create($validated)** で、検証されたデータを元に新しい投稿を作成します。


	•  **Post::create()** は、Postモデルのインスタンスを作成し、そのインスタンスをデータベースに挿入するメソッドです。 
    •  **$validated** には、titleやbodyなど、フォームから送信された検証済みのデータが含まれています。 
	•  このメソッドを実行すると、新しいレコードがpostsテーブルに追加されます。 
	•  返り値として作成されたPostモデルのインスタンス（新しい投稿オブジェクト）が返され、それが$post変数に格納されます。


**$request->session()->flash('message', '保存しました');**
1.	成功した場合、セッションに「保存しました」というメッセージを設定し、フォームに戻します。
•  session()->flash()メソッドを使って、一時的なセッションデータを設定します。これは、リダイレクト後に次のリクエストで利用できるデータです。 

•  'message'はセッションキー、'保存しました'はその値です。これにより、次のページ読み込み時にセッションから「保存しました」というメッセージが取得できます。 
1.	•  フラッシュメッセージは一度しか表示されません。セッションが新しいリクエストに対して送信された後、消去されます。

```php
public function show(Post $post){
    return view('post.show', compact('post')); // 特定の投稿を表示
}
```

•  **目的** : 投稿の詳細を表示します。 
•  **処理** : 
•	ルートパラメータで渡されたPostモデル（$post）をビューに渡して表示します。

```php
public function edit(Post $post){
    return view('post.edit', compact('post')); // 特定の投稿を編集するフォームを表示
}
```

•  **目的**: 投稿を編集するためのフォームを表示します。 
•  **処理**: 
•	Postモデルをビューに渡して、投稿の編集フォームを表示します。


```php
public function update(Request $request, Post $post){
    $validated = $request->validate([
        'title' => 'required|max:20',
        'body' => 'required|max:400',
        'image' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048'
    ]);

    $validated['user_id'] = auth()->id(); // 更新するユーザーIDを設定

    if ($request->hasFile('image')) {
        // 既存の画像を削除
        if ($post->image) {
            Storage::disk('public')->delete($post->image);
        }
        // 新しい画像を保存
        $validated['image'] = $request->file('image')->store('images', 'public');
    }

    // 投稿データを更新
    $post->update($validated);

    // セッションメッセージを設定
    $request->session()->flash('message', '更新しました');
    return back();
}
```

•	**目的**: 投稿を更新します。
•	**処理**: 
 o	バリデーションを行い、タイトル、本文、画像の検証を行います。
 o	画像がアップロードされていれば、既存の画像を削除し、新しい画像を保存します。
 o	Post::update()でデータを更新し、セッションメッセージを設定します。

```php
public function destroy(Request $request, Post $post){
    if ($post->image) {
        Storage::disk('public')->delete($post->image); // 投稿に画像があれば削除
    }

    $post->delete(); // 投稿を削除
    $request->session()->flash('message', '削除しました'); // セッションメッセージ
    return redirect()->route('post.index'); // 投稿一覧へリダイレクト
}
```

•  **目的**: 投稿を削除します。 
•  **処理**: 
•	投稿に画像があれば、それを削除します。
•	Post::delete()で投稿を削除します。
•	セッションに'削除しました'というメッセージを設定し、投稿一覧にリダイレクトします。