Laravel9 | CRUDアプリに画像登録機能を追加する方法

先日投稿したCRUDアプリに画像ファイルを1枚追加できる機能を実装する方法のメモ。

DBに画像ファイル用のカラム追加

マイグレーションファイル作成

sail artisan make:migration add_image_to_posts_table --table=posts
INFO  Migration [database/migrations/2023_01_24_135530_add_image_to_posts_table.php] created successfully.

マイグレーションファイル編集

upメソッド編集

カラム追加
# /database/migrations/2023_01_24_135530_add_image_to_posts_table.php

// カラム追加(descriptionの後に追加、NULL許可)
$table->text('image')->after('description')->nullable();
編集後のupメソッド
# /database/migrations/2023_01_24_135530_add_image_to_posts_table.php

public function up()
{
    Schema::table('posts', function (Blueprint $table) {

        // カラム追加(descriptionの後に追加、NULL許可)
        $table->text('image')->after('description')->nullable();

        // カラム追加(テーブルの最後に追加)
        // $table->text('image');
        
        // カラム追加(指定したカラムの後に追加)
        // $table->text('image')->after('description');

        // NULL許可
        // $table->string('image')->nullable();
    });
}

downメソッド編集

カラム削除
# /database/migrations/2023_01_24_135530_add_image_to_posts_table.php

// カラム削除
$table->dropColumn('image');
編集後のdownメソッド
# /database/migrations/2023_01_24_135530_add_image_to_posts_table.php

public function down()
{
    Schema::table('posts', function (Blueprint $table) {
        // カラム削除
        $table->dropColumn('image');
    });
}

マイグレーション実行

sail artisan migrate
INFO  Running migrations.  
2023_01_24_135530_add_image_to_posts_table ............... 49ms DONE

ロールバック(*必要な場合のみ)

sail artisan migrate:rollback
INFO  Rolling back migrations.  
2023_01_24_135530_add_image_to_posts_table ............... 43ms DONE

シンボリックリンク作成

フォームから登録した画像ファイルが参照できるようシンボリックリンクを作成します。

sail artisan storage:link
INFO  The [public/storage] link has been connected to [storage/app/public].

Postモデル編集

fillable設定

# /app/Models/Post.php

// fillable に image を追加
protected $fillable = ['title', 'description', 'image'];

PostRequest編集

rulesメソッド編集

バリデーションルール追加

# /app/Http/Requests/PostRequest.php

// 画像ファイル用バリデーションルールを追加する
'image' => ['nullable', 'max:1024', 'mimes:jpg,jpeg,png,gif'], // 画像

編集後のrulesメソッド

# /app/Http/Requests/PostRequest.php

public function rules()
{
    return [
        'title'       => ['required', 'max:80'],                             // タイトル
        'description' => ['required'],                                       // 本文
        'image'       => ['nullable', 'max:1024', 'mimes:jpg,jpeg,png,gif'], // 画像
    ];
}

PostController編集

画像保存処理 store()

画像取得

# /app/Http/Controllers/PostController.php

// 画像取得
$image_path    = $request->file('image')->store('image', 'public');
$data['image'] = $image_path;

編集後のstoreメソッド

# /app/Http/Controllers/PostController.php

public function store(PostRequest $request)
{
    // 保存用データ配列
    $data = $request->validated();

    // 画像取得
    $image_path = '';
    if ($request->hasFile('image')) {
        $image_path    = $request->file('image')->store('image', 'public');
        $data['image'] = $image_path;
    }

    // 保存
    Post::create($data);

    return redirect()->route('posts.index')->with('message', '投稿の作成が完了しました。');
}

画像更新処理 update()

use宣言

# /app/Http/Controllers/PostController.php

// use宣言(Storageクラス利用)
use Illuminate\Support\Facades\Storage;

既存画像削除と新しい画像取得

# /app/Http/Controllers/PostController.php

// 現在の画像ファイル削除
if ($iamge_cur !== '' && !is_null($iamge_cur)) {
    Storage::disk('public')->delete($iamge_cur);
}
// 選択画像ファイルを保存してパスをセット
$image_path    = $request->file('image')->store('image', 'public');
$data['image'] = $image_path;

編集後のupdateメソッド

# /app/Http/Controllers/PostController.php

public function update(PostRequest $request, Post $post)
{
    //
    $image_path = '';           // 選択された画像
    $image_cur  = $post->image; // 現在の画像ファイルパス

    // 保存用データ配列
    $data = [
        'title'       => $request->title,
        'description' => $request->description,
    ];

    // 
    if ($request->hasFile('image')) {
        // 現在の画像ファイル削除
        if ($image_cur !== '' && !is_null($image_cur)) {
            Storage::disk('public')->delete($image_cur);
        }
        // 選択画像ファイルを保存してパスをセット
        $image_path    = $request->file('image')->store('image', 'public');
        $data['image'] = $image_path;
    }

    // 更新
    $post->update($data);

    return redirect()->route('posts.index')->with('message', '投稿の更新が完了しました。');
}

画像削除処理 destroy()

画像削除

# /app/Http/Controllers/PostController.php

// 画像ファイルパスを取得
$image_cur = $post->image;

// 登録されていれば削除
if ($image_cur !== '' && !is_null($image_cur)) {
    Storage::disk('public')->delete($image_cur);
}

編集後のdestroyメソッド

# /app/Http/Controllers/PostController.php

public function destroy(Post $post)
{
    // 画像ファイルパスを取得
    $image_cur = $post->image;

    // 登録されていれば削除
    if ($image_cur !== '' && !is_null($image_cur)) {
        Storage::disk('public')->delete($image_cur);
    }

    // 削除
    $post->delete();

    return redirect()->route('posts.index')->with('message', '投稿の削除が完了しました。');
}

Bladeテンプレート編集

create.blade.php

<x-app>

  <form method="POST" action="{{ route('posts.index') }}" enctype="multipart/form-data">
    @csrf

    <div>
      <div>
        <label>タイトル</label>
      </div>
      <div>
        <input type="text" name="title" value="{{old('title')}}">
      </div>
      @error('title')
      <span>{{ $message }}</span>
      @enderror
    </div>

    <div>
      <div>
        <label>本文</label>
      </div>
      <div>
        <textarea name="description" rows="4">{{old('description')}}</textarea>
      </div>
      @error('description')
      <span>{{ $message }}</span>
      @enderror
    </div>

    <div>
      <div><label>画像</label></div>
      <div>
        <input type="file" name="image">
      </div>
      @error('image')
      <span>{{ $message }}</span>
      @enderror
    </div>

    <button type="submit">保存</button>

  </form>

</x-app>

edit.blade.php

<x-app>

  <form method="POST" action="{{ route('posts.update',$post->id) }}" enctype="multipart/form-data">
    @csrf
    @method('PUT')

    <div>
      <div>
        <label>タイトル</label>
      </div>
      <div>
        <input type="text" name="title" value="{{old('title',$post->title)}}">
      </div>
      @error('title')
      <span>{{ $message }}</span>
      @enderror
    </div>

    <div>
      <div>
        <label>本文</label>
      </div>
      <div>
        <textarea name="description" rows="4">{{old('description',$post->description)}}</textarea>
      </div>
      @error('description')
      <span>{{ $message }}</span>
      @enderror
    </div>


    <div>
      <div><label>画像</label></div>
      <div>
        <div>
          @if ($post->image !=='')
          <img src="{{ Storage::url($post->image) }}">
          @else
          @endif
        </div>
        <div>
          <input type="file" name="image">
        </div>
        @error('image')
        <span>{{ $message }}</span>
        @enderror
      </div>


      <button type="submit">更新</button>

  </form>

</x-app>

show.blade.php

<x-app>

  <div>
    <label>タイトル</label>
    <p>{{old('title',$post->title)}}</p>
  </div>

  <div>
    <label>本文</label>
    <p>{{old('description',$post->description)}}</p>
  </div>

  <div>
    <label>画像</label>
    @if ($post->image !=='')
    <div>
      <img src="{{ Storage::url($post->image) }}">
    </div>
    @else
    @endif
  </div>

  <button onclick="location.href='{{ route('posts.index') }}'">戻る</button>

</x-app>

index.blade.php

<x-app>

  @if (session()->has('message'))
  <div>{{ session('message') }}</div>
  @endif

  <div>
    <button onclick="location.href='{{ route('posts.create') }}'" >新規作成</button>
  </div>

  <table>
    <thead>
      <tr>
        <th>ID</th>
        <th>タイトル</th>
        <th>本文</th>
        <th>画像</th>
        <th>作成日時</th>
        <th>更新日時</th>
        <th colspan="2">編集・削除</th>
      </tr>
    </thead>

    <tbody>

      @foreach ($posts as $post)
      <tr>
        <td>{{ $post->id }}</td>
        <td><a href="{{ route('posts.show', $post->id) }}">{{ $post->title }}</a></td>
        <td>{!! nl2br( htmlspecialchars($post->description) ) !!}</td>
        <td>@if ($post->image !=='')<img src="{{ Storage::url($post->image) }}" width="50%">@else @endif</td>
        <td>{{ $post->created_at }}</td>
        <td>{{ $post->updated_at }}</td>
        <td>
          <button onclick="location.href='{{ route('posts.edit', $post->id) }}'" >編集</button>
        </td>
        <td>
          <form action="{{ route('posts.destroy',$post->id) }}" method="POST" onsubmit="return confirm('削除してもよろしいですか?');">
            <input type="hidden" name="_method" value="DELETE">
            <input type="hidden" name="_token" value="{{ csrf_token() }}">
            <button type="submit">削除</button>
          </form>
        </td>
      </tr>
      @endforeach

    </tbody>
  </table>

</x-app>

動作環境情報

"macOS Ventura" 13.1
"Docker Desktop" 4.15.0
"Laravel Sail"
"Laravel Framework" 9.48.0

関連記事

コメント

タイトルとURLをコピーしました