Livewireで日本語ファイル名の扱い方: S3ストレージとの連携

## 使用技術
- Laravel
- Livewire
- S3

## エラーが発生した状況

<img width="200" alt="スクリーンショット 2024-06-07 10.11.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3012370/5b2c51de-4085-fc0d-9d93-00d119834d30.png">

アップロードした画像をLivewireのtemporyUrl（）でプレビュー機能を実装しようとしましたが、画像が表示されませんでした。
デフォルトのファイル名に`スクリーンショット 2024-06-07 10.11.45.png`となり日本語が含まれていました。

## エラー内容
`Header value cannot be represented using ISO-8859-1. `
![スクリーンショット 2024-06-07 10.11.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3012370/3d126037-c3fc-2dd1-6467-8e235abc6b2f.png)
このエラーは、ResponseContentDisposition ヘッダーにISO-8859-1文字セットで表現できない文字（この場合は日本語）が含まれているために発生しています。AWS S3はデフォルトでISO-8859-1エンコーディングを期待しており、それ以外の文字が含まれると InvalidArgument エラーを返します。
#### ISO-8859-1エンコーディング
ISO-8859-1エンコーディングは、特に西ヨーロッパの言語に適した文字エンコーディング方式です。。このエンコーディングは、ISO（国際標準化機構）によって定められたもので、ASCIIに含まれる128文字に加え、さらに追加の128文字（主に西ヨーロッパの特殊文字やアクセント記号が含まれる）を含む合計256文字を表現できます。

## エラー発生時のコード
```Livewire/EditImage.php
<?php

namespace App\Livewire
use Livewire\Component;
use Livewire\WithFileUploads;

class EditImage extends Component
{
    use WithFileUploads;

    public $newImage;
}
```

```php
    <div class="w-[100px] h-[100px] bg-gray-100 rounded-lg border border-gray-300 mb-4 overflow-hidden">
     @if($newImage)
        <img src="{{ $newImage->temporaryUrl() }}" class="w-full h-full object-cover">
     @end
    </div>

    <input type="file" accept="image/png, image/jpeg, image/svg+xml" id="image" wire:model="newImage" class="hidden">
        <label for="image" class="w-[158px] h-[40px] bg-button-base-l text-button-medium text-primary font-bold py-2 px-4 rounded-lg border border-gray-300 focus:outline-none hover:bg-gray-100 cursor-pointer">
            画像アップロード
        </label>
```

#### temporaryUrl
LiveWireのtemporaryUrl()メソッドは、ファイルアップロード機能を利用している際に、アップロードされたファイルの一時的なプレビューURLを生成するために使用されます。
ユーザーがファイルを選択した後、フォームを送信してファイルを保存する前に、そのファイルのプレビューをユーザーに表示できます。

https://livewire.laravel.com/docs/uploads#temporary-preview-urls:~:text=%23-,Temporary%20preview%20URLs,-After%20a%20user

## 解決策
blade内で`temporaryUrl`を呼び出すのではなく、ファイルが選択されたらLivewireのコンポーネント側でメソッドを発火させます。メソッド内でエンコーディングし、それをbladeに渡す方法で実装をします。
#### 画像が選択されたら、自動的にメソッドを発火させる
Livewire でコンポーネントのパブリックプロパティが更新されると、自動的に対応するライフサイクルフックがトリガーされるため、特定のプロパティの変更を検知して所定のアクションを実行することができます。
プロパティ名を指定した `updated{PropertyName}` メソッドを定義することで、そのプロパティの更新後に自動的にメソッドが呼び出されます。

https://livewire.laravel.com/docs/lifecycle-hooks#update

たとえば、`newImage` プロパティが変更されたときに何か処理を行いたい場合、以下のように `updatedNewImage` メソッドをコンポーネントに定義します：
```Livewire/EditImage.php

 public $newImage;

 public function updatedNewImage()
 {
    $temporaryUrl = $this->getTemporaryUrl($this->newImage);

    $this->newImageTemporaryUrl = $temporaryUrl;
 }
```

上記のメソッドを`Livewire/EditImage.php`に追加します。

```Livewire/EditImage.php
<?php

namespace App\Livewire
use Livewire\Component;
use Livewire\WithFileUploads;

class EditImage extends Component
{
    use WithFileUploads;

    public $newImage;

    public $newImageTemporaryUrl;//追加

    /**
     * 新しいメイン画像ファイルがアップロードされたときに呼び出される。
     * アップロードされた画像ファイルに対して一時的なURLを生成し、
     * そのURLをコンポーネントの状態に保存する。
     */
    public function updatedNewImage()
    {
        $temporaryUrl = $this->getTemporaryUrl($this->newImage);
    
        $this->newImageTemporaryUrl = $temporaryUrl;
    }
    
     /**
     * アップロードされたファイルを一時的にS3に保存し、
     * そのファイルへの一時的な署名付きURLを生成する。
     * このURLは、指定された時間（ここでは5分）後にアクセスできなくなる。
     *
     * @param UploadedFile $file アップロードされたファイル
     * @return string 生成された一時的なURL
     */
    protected function getTemporaryUrl($file)
    {
        $filename = $file->getClientOriginalName();
        $encodedFilename = rawurlencode($filename);
        $path = $file->storeAs('temp', $encodedFilename, 's3');
    
        return Storage::disk('s3')->temporaryUrl(
            $path,
            now()->addMinutes(5),
                ['ResponseContentDisposition' => "attachment; filename*=UTF-8''{$encodedFilename}"]
        );
    }
}
```

## コードの修正点
### 1.ファイル名の取得とエンコード
- `getClientOriginalName()` メソッドを使って、アップロードされたファイルのオリジナルのファイル名を取得します。
- `rawurlencode` はURLに安全な形式で文字をエンコードします。これにより、日本語などの非ASCII文字も含めることができます。
### 2.S3へのファイルアップロード
- `storeAs('temp', $encodedFilename, 's3')` メソッドを使って、エンコードされたファイル名を使用し、S3の temp ディレクトリにファイルをアップロードします。ここで 's3' はファイルシステムのディスク名であり、`config/filesystems.php` で定義されているS3ドライバーを指します。

https://arc.net/l/quote/ibddltbu

### 3.一時URLの生成
- `Storage::disk('s3')->temporaryUrl()` メソッドを使用して、アップロードされたファイルの一時アクセスURLを生成します。このURLは、生成から指定された時間（この場合は5分）で期限切れになります。
- 第二引数の now()->addMinutes(5) は、現在時刻から5分後にURLの有効期限が設定されることを意味します。
- `filename*=` を使用してファイル名を設定することで、RFC 5987の要件に従い、非ASCII文字を含むファイル名を正しく扱うことができます。

https://www.silicloud.com/ja/blog/http%E3%83%98%E3%83%83%E3%83%80%E3%83%BC%E3%81%AEcontent-disposition%E3%81%AE%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E5%90%8D%E3%81%8C%E5%8C%96%E3%81%91%E3%82%8B/


最後に、画像が選択されたら`EditImage `コンポーネントで定義した`$newImageTemporaryUrl`をbladeに渡すように変更します。
```php
    <div class="w-[100px] h-[100px] bg-gray-100 rounded-lg border border-gray-300 mb-4 overflow-hidden">
      @if($newImage)
        //ここを変更し、newImageTemporaryUrlを渡す
        <img src="{{ $newImageTemporaryUrl }}" class="w-full h-full object-cover">
      @end
    </div>

    <input type="file" accept="image/png, image/jpeg, image/svg+xml" id="image" wire:model="newImage" class="hidden">
        <label for="image" class="w-[158px] h-[40px] bg-button-base-l text-button-medium text-primary font-bold py-2 px-4 rounded-lg border border-gray-300 focus:outline-none hover:bg-gray-100 cursor-pointer">
            画像アップロード
        </label>
```