Laravel10でS3の画像を扱う

Laravel10でS3に画像をアップロードしたり、S3の画像を表示する処理を書いたのでメモとしてまとめておくことにしました。

## S3バケットの用意
バケットを作成します。

バケット名を入力し、パブリックアクセスのチェックを外します。これ以外はデフォルトのままバケットを作成します。
![スクリーンショット 2024-06-10 23.43.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1295926/3c50ae68-22f6-4619-dd67-77580d038fb4.png)

## IAMの作成
S3を操作するためのIAMユーザーを作成します。ポリシーは必要なものをアタッチすれば大丈夫です。
![スクリーンショット 2024-06-10 23.48.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1295926/e11349c8-ae68-bc99-8fc6-ed0927bb518e.png)

セキュリティ認証情報のタブからアクセスキーを作成しておきます。.csvファイルも念のため保存しておくと良いです。

## バケットポリシーの設定
S3のバケットを選択し、アクセス許可のタブを開きます。
バケットポリシーの項目から編集ボタンを押します。
ポリシージェネレーターを使用して作成すると簡単です。こんな感じで入力しました。
![スクリーンショット 2024-06-10 23.54.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1295926/d9f8bf22-16dd-d0bf-7148-441f463b7414.png)
下のようなポリシーが生成されます。
```
{
    "Version": "2012-10-17",
    "Id": "policyId xxxx",
    "Statement": [
        {
            "Sid": "xxxxxxxxxx",
            "Effect": "Allow",
            "Principal": {
                "AWS": "IAMユーザーのarn"
            },
            "Action": "s3:*",
            "Resource": "s3バケットのarn"
        }
    ]
}
```

## Laravelの準備
以下のライブラリを使用するのでインストールします。[flysystem-aws-s3-v3](https://github.com/thephpleague/flysystem-aws-s3-v3)
`composer require league/flysystem-aws-s3-v3`

追記します。
```/config/filesystems.php
  'cloud' => env('FILESYSTEM_CLOUD', 's3'),
  's3' => [
            'driver' => 's3',
            'key' => env('AWS_ACCESS_KEY_ID'),
            'secret' => env('AWS_SECRET_ACCESS_KEY'),
            'region' => env('AWS_DEFAULT_REGION'),
            'bucket' => env('AWS_BUCKET'),
            'url' => env('AWS_URL'),
            'endpoint' => env('AWS_ENDPOINT'),
    ],
```

.envにも必要な情報を追記します。
```.env
AWS_ACCESS_KEY_ID='IAMユーザーのAccess key ID'
AWS_SECRET_ACCESS_KEY='IAMユーザーのSecret access key'
AWS_DEFAULT_REGION=ap-northeast-1
AWS_BUCKET='作成したバケット名'
```

コントローラーを作成し以下の処理を記述しておきます。
`php artisan make:controller ImageUploadController` 
```php
    public function showUploadForm()
    {
        return view('/upload');
    }

    public function uploadImage(Request $request)
    {
        $request->validate([
            'image' => 'required|image|mimes:jpeg,png,jpg,gif|max:2048',
        ]);

        $file = $request->file('image');

        $fileName = time() . '.' . $file->getClientOriginalExtension();
        $path = 'images/' . $fileName;

        $result = Storage::disk('s3')->put($path, file_get_contents($file));
        if (!$result) {
            Log::error('File could not be uploaded to S3.');
            return response()->json(['error' => 'File could not be uploaded'], 500);
        }
        // 公開URLを取得
        $url = Storage::disk('s3')->url($path);
        return back()->with('success', 'Image uploaded successfully.')->with('url', $url);
    }
```

web.phpにroutingも追記します
```php
Route::get('/upload', [ImageUploadController::class, 'showUploadForm']);
Route::post('/upload', [ImageUploadController::class, 'uploadImage'])->name('uploadToS3');
```

viewも簡単に作成しておきます。
```html
<body>
    @if ($message = Session::get('success'))
        <div>
            <strong>{{ $message }}</strong>
        </div>
        <div>
            <img src="{{ Session::get('url') }}" width="300" />
        </div>
    @endif

    @if (count($errors) > 0)
        <div>
            <strong>Whoops!</strong> There were some problems with your input.
            <ul>
                @foreach ($errors->all() as $error)
                    <li>{{ $error }}</li>
                @endforeach
            </ul>
        </div>
    @endif

    <form action="{{ route('uploadToS3') }}" method="POST" enctype="multipart/form-data">
        @csrf
        <div>
            <input type="file" name="image" />
        </div>
        <div>
            <button type="submit">Upload</button>
        </div>
    </form>
</body>
```

## 動作確認
ファイルを選択してアップロードします。Uploadボタンを押すとバケットにimageフォルダが作成され、その中にアップロードした画像を確認できます。

![スクリーンショット 2024-06-11 0.03.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1295926/a0eb891a-b592-1153-6dd8-8c2e8ef2c5a3.png)
![スクリーンショット 2024-06-11 0.07.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1295926/39a19b54-8df7-bbd7-428e-20f4544ebbd6.png)

権限まわりで若干悩みましたが意外と簡単にできました。