Laravel laravel-medialibraryで S3に画像保存

## AWSのアクセスキーを取得
※IAMが作成されている前提です。
AWSコンソール画面に行き、IAMを選択します。サイドバーのユーザーを選択し、任意のユーザーを選択します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3012370/0ce89684-f0e1-7617-d00e-e1c3d04d053d.png" width=500>

セキュリティ認証情報のタブからアクセスキーの欄に行き、アクセスキーを作成するをクリックします。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3012370/705119c6-80c2-1296-ede8-6a299c80f2d6.png" width=500>

※アクセスキーは一度しか表示されないので.envファイルに貼り付けてください。
```.env
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
```

## バケット作成
#### S3からバケットを作成します。
![スクリーンショット 2024-10-10 9.26.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3012370/8b7daf13-3682-3031-f69c-45d6a8221584.png)

#### 一意のバケット名を入力
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3012370/937d7350-092b-e2e9-62a9-943e75e92c71.png" width=500>

#### パブリックアクセスを全てブロックを外します
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3012370/04033754-6dc4-7b99-caca-8c95a85b3277.png" width=500>

その他はデフォルトの設定でいいです。

そして下部までいってバケットを作成ボタンを押してください。

## エラー時の対応
#### ポリシーの設定を行わないと画像にアクセスした際に権限エラーとなります。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3012370/e3b9b124-b84a-0f29-a73e-9c8932660d90.png" width=700>

#### ポリシーの設定
バケット->アクセス許可　からバケットポリシーを設定します。
ポリシージェネレーターをクリックします。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3012370/82972e4b-df51-a0cb-5a05-d1d165becc89.png" width=600>

- Step1。Select Type of Policyを S3 Bucket Policyにします。
- Step2。Principalに* を入力します。
- Step3。ActionsからGetObjectとGetObjectVersionを選択します。
- Step4。ARMを入力してください。最後尾に/*　をつけることを忘れないでください。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3012370/e8ac90e8-88bb-e997-d284-6d643c843ea0.png" width=600>

```
{
    "Version": "2012-10-17",
    "Id": "Policy********",
    "Statement": [
        {
            "Sid": "Stmt********",
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:GetObject",
                "s3:GetObjectVersion"
            ],
            "Resource": "arn:aws:s3:::hoge/*"
        }
    ]
}
```

## 実装
```
AWS_ACCESS_KEY_ID=取得したアクセスキー
AWS_SECRET_ACCESS_KEY=取得したシークレットキー
AWS_DEFAULT_REGION=ap-northeast-1
AWS_BUCKET=作成したバケット名
AWS_USE_PATH_STYLE_ENDPOINT=false
```

#### Laravel-medialibraryを導入
```
composer require "spatie/laravel-medialibrary"
```
```
php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider"

php artisan migrate
```

https://spatie.be/docs/laravel-medialibrary/v11/installation-setup

#### flysystem-aws-s3を導入
```
composer require league/flysystem-aws-s3-v3:^3.0
```
https://flysystem.thephpleague.com/docs/adapter/aws-s3-v3/

### モデルに記述
使用するモデルに`HasMedia` と`InteractsWithMedia`をインポートします。
```php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;

class User extends Model implements HasMedia //こちら
{
    use InteractsWithMedia;//こちら
}
```

### View
![スクリーンショット 2024-10-12 14.15.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3012370/dbbd3a07-cd13-2d83-d5de-0f0c289b56d0.png)

```php
<form class="space-y-6" action="{{ route('user.image_upload') }}" method="POST" enctype="multipart/form-data" >
    @method('PUT')
    <div class="col-span-full">
        <label for="cover-photo" class="block text-sm font-medium leading-6 text-gray-900">画像</label>
        <div class="mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 px-6 py-10">
            <div class="text-center">
                <svg class="mx-auto h-12 w-12 text-gray-300" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
                    <path fill-rule="evenodd" d="M1.5 6a2.25 2.25 0 012.25-2.25h16.5A2.25 2.25 0 0122.5 6v12a2.25 2.25 0 01-2.25 2.25H3.75A2.25 2.25 0 011.5 18V6zM3 16.06V18c0 .414.336.75.75.75h16.5A.75.75 0 0021 18v-1.94l-2.69-2.689a1.5 1.5 0 00-2.12 0l-.88.879.97.97a.75.75 0 11-1.06 1.06l-5.16-5.159a1.5 1.5 0 00-2.12 0L3 16.061zm10.125-7.81a1.125 1.125 0 112.25 0 1.125 1.125 0 01-2.25 0z" clip-rule="evenodd" />
                </svg>
                <div>
                    <label for="image">画像を選択:</label>
                    <input type="file" id="image" name="image" accept="image/*">
                </div>
                <div class="file-name" id="file-name">選択されたファイル: なし</div>
                    <p class="text-xs leading-5 text-gray-600">PNG, JPG, GIF up to 10MB</p>
                </div>
            </div>
        </div>
    </div>
    <button type="submit" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
        追加
    </button>
</form>
```
### ルートを作成
```php
Route::controller(UserImageController::class)->group(function () {
    Route::get('image/create', 'index')->name('user.image_upload_form');
    Route::post('image/store', 'store')->name('user.image_upload');
});
```

### コントローラーを作成
```php
<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\View\View;
use Illuminate\Http\Request;
use Illuminate\Support\Str;

class UserImageController extends Controller
{

    /**
     * 画像追加フォームを表示
     */
    public function create(): View
    {
        return view('user.image_upload');
    }

     /**
     * 商品を追加する
     */
    public function store(Request $request)
    {
        $validated = $request->validate([
            'image' => 'required|image|mimes:jpeg,png,jpg,gif,svg',
        ]);

        $image = User::create($validated);

        // メディアの追加
        if ($request->hasFile('image')) {
            $filePath = Str::uuid() . '.' . $request->file('image')->getClientOriginalExtension();
            $item->addMediaFromRequest('image')
                 ->usingFileName($filePath)
                 ->toMediaCollection('images', 's3');
        }

        return redirect()->route('user.image_upload_form');
    }
}
```

このままですと、[id]/ファイル名のように作成されてしまいます。今回の意図はコレクション名/ファイル名で保存したいのでカスタムパスジェネレーターを作成して設定を行います。

### Laravel Medialibrary の設定ファイルを作成
```
php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="config"
```

### カスタムパスジェネレータを作成
```php
<?php

namespace App\PathGenerators;

use Spatie\MediaLibrary\Support\PathGenerator\PathGenerator;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
use Illuminate\Support\Str;

class CustomPathGenerator implements PathGenerator
{
    /**
     * メディアファイルの基本パスを取得します。
     *
     * @param \Spatie\MediaLibrary\MediaCollections\Models\Media $media
     * @return string
     */
    public function getPath(Media $media): string
    {
        // モデルの短縮クラス名（例: 'User'）を取得
        $modelName = class_basename($media->model);

        // モデル名を複数形に変換し、小文字にする（例: 'users'）
        $folderName = Str::plural(Str::snake($modelName));

        return "{$folderName}/";
    }

    /**
     * メディアの変換ファイルのパスを取得します。
     *
     * @param \Spatie\MediaLibrary\MediaCollections\Models\Media $media
     * @return string
     */
    public function getPathForConversions(Media $media): string
    {
        $modelName = class_basename($media->model);
        $folderName = Str::plural(Str::snake($modelName));

        return "{$folderName}/conversions/";
    }

    /**
     * レスポンシブ画像のパスを取得します。
     *
     * @param \Spatie\MediaLibrary\MediaCollections\Models\Media $media
     * @return string
     */
    public function getPathForResponsiveImages(Media $media): string
    {
        $modelName = class_basename($media->model);
        $folderName = Str::plural(Str::snake($modelName));

        return "{$folderName}/responsive/";
    }
}
```

### カスタムパスジェネレータを登録

```configmedia-library.php
<?php

return [
  'path_generator' => App\PathGenerators\CustomPathGenerator::class,
]
```

## 表示の仕方
```php
@if($recruit->hasMedia('images')
    <img src="{{ $recruit->getFirstMediaUrl('images') }}"  class="w-32 h-32 object-contain">
@endif


## 更新処理
```php
public function update(Request $request, User $user)
    {
        $validated = $request->validate([
            'image' => 'required|image|mimes:jpeg,png,jpg,gif,svg',
        ]);
        
        $user->update($validated);

        // メディアの更新
        if ($request->hasFile('image')) {
            // 既存の画像を削除（オプション）
            if ($user->hasMedia('images')) {
                $user->clearMediaCollection('images');
            }

            // 新しい画像をアップロード
            $user->addMediaFromRequest('image')
                     ->usingFileName(Str::uuid() . '.' . $request->file('image')->getClientOriginalExtension())
                     ->toMediaCollection('images', 's3');
        }
    }
```