Laravel laravel-medialibraryで 画像保存からAlpine.jsでプレビューするまで

画像保存ライブラリにlaravel-medialibraryを使用しました。

https://github.com/spatie/laravel-medialibrary

https://spatie.be/docs/laravel-medialibrary/v11/introduction

Illuminate\Da
## 導入

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

```
composer require "spatie/laravel-medialibrary:^11.0.0"
```

```
php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider"

php artisan migrate
```
以下のマイグレーションファイルが作成されます。
```php
<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('media', function (Blueprint $table) {
            $table->id();

            $table->morphs('model');
            $table->uuid('uuid')->nullable()->unique();
            $table->string('collection_name');
            $table->string('name');
            $table->string('file_name');
            $table->string('mime_type')->nullable();
            $table->string('disk');
            $table->string('conversions_disk')->nullable();
            $table->unsignedBigInteger('size');
            $table->json('manipulations');
            $table->json('custom_properties');
            $table->json('generated_conversions');
            $table->json('responsive_images');
            $table->unsignedInteger('order_column')->nullable()->index();

            $table->nullableTimestamps();
        });
    }
};
```

メディアをモデルに関連付けるには、モデルが次のインターフェイスと特性を実装する必要があります。

https://spatie.be/docs/laravel-medialibrary/v11/basic-usage/preparing-your-model

```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;//追加
}
```
## カスタムパスジェネレータを作成

https://spatie.be/docs/laravel-medialibrary/v11/advanced-usage/using-a-custom-directory-structure

デフォルトでは、ファイルはメディア オブジェクトの ID を名前として使用するディレクトリ内に保存されます。

```
media
---- 1
------ file.jpg
------ conversions
--------- small.jpg
--------- medium.jpg
--------- big.jpg
```
ですが、`モデル名/id/logs/ファイル名`にしたかったので保存先をカスタマイズしました。
#### カスタム パス ジェネレーターを作成します。
`getPath`メソッドで保存先のディレクトリを指定しています。

`PathGenerator`インターフェースをオーバーライドした、`CustomPathGenerator`クラスを作成します。
```app/PathGenerators/CustomPathGenerator.php
<?php

namespace App\PathGenerators;

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

class CustomPathGenerator implements PathGenerator
{
    /**
     * 関連付けされているモデル名を取得し、例company/インスタンスのid/logos/。のような保存先パスを指定。
     */
    public function getPath(Media $media): string
    {
        return strtolower(class_basename($media->model_type)) . '/' . $media->model_id . '/' . $media->collection_name . '/';
    }

    /**
     * 指定されたメディアオブジェクトの変換ファイル（サムネイル、リサイズされた画像など）を保存するためのパスを返す。
     */
    public function getPathForConversions(Media $media): string
    {
        return $this->getPath($media).'conversions/';
    }

    /**
     * 異なる画面サイズに対応するためのイメージを保存するパスを生成。
     */
    public function getPathForResponsiveImages(Media $media): string
    {
        return $this->getPath($media).'responsive-images/';
    }
}

```
次に、このカスタムパスジェネレータをLaravel MediaLibraryに登録する必要があります。これを行うには、config/medialibrary.php 設定ファイルを編集し、path_generator キーの値をカスタムパスジェネレータクラスに設定します。
```php
'path_generator' => App\PathGenerators\CustomPathGenerator::class,
```

```
user
---- 1
------ logs
--------- small.jpg
---- 2
------ logs
--------- sample.jpg
```
モデルのidごとに任意のディレクトリ名(この場合は`logs`)で保存されるようになります。
## コントローラー保存処理
全体のコード
```php
public function update(Request $request)
{

    //validation処理省略


    if ($request->hasFile('logo')) {
            // 既存のロゴ画像を削除して新しい画像を保存
            $company->clearMediaCollection('logos');
            $company->addMediaFromRequest('logo')->toMediaCollection('logos');
        }


    //リダイレクト処理
}
```
今回はプロフィールにロゴを持たせたので、更新処理に記述しました。
工夫した点は、このままだと既存の画像をファイルに残したまま新しく画像を追加してしまい、ストレージを余分に使用してしまいます。
```php
if ($request->hasFile('logo')) {
        $company->addMediaFromRequest('logo')->toMediaCollection('logos');
    }
```
無駄なストレージを使うのを防ぐために下記を追加して、一旦全て削除し上書きされるようにしました。
```php
  $company->clearMediaCollection('logos');
```

## viewに表示
```blade.php
<img src="{{ $company->getFirstMediaUrl('logos') }}">
```


## Alpine.jsでファイル選択時にプレビュー
Alpine.jsを導入してある前提です。

通常はファイル選択しても、画像は表示されずファイル名のみが表示されます。
画像機能としては果たしていますが、選択した画像が表示される方がUXがいいと思い実装しました。
また、tailwindcssでレイアウトをしました。

まず、`logoPreview`を定義します。`input type=file`がクリックされると`@change`によって画像のURLを取得するロジックが発火され、`logoPreview`に画像のURLが入ります。
```blade.php
//画像がある場合は初期値にその画像のURLをlogoPreviewに入れいます
 <div x-data="{ logoPreview: '{{ $user->hasMedia('logos') ? $user->getFirstMediaUrl('logos') : '' }}' }" class="sm:grid sm:grid-cols-3 sm:items-center sm:gap-4 sm:py-6">
        <label for="photo" class="block text-sm font-medium leading-6 text-gray-900">ロゴ</label>
        <div class="mt-2 sm:col-span-2 sm:mt-0">
            <div class="flex items-center gap-x-3">
                <label for="logo" class="relative cursor-pointer rounded-md bg-white font-semibold text-indigo-600 focus-within:outline-none focus-within:ring-2 focus-within:ring-indigo-600 focus-within:ring-offset-2 hover:text-indigo-500">
                    <span>画像をアップロード</span>
                    <input id="logo" name="logo" type="file" class="sr-only" @change="
                        logoPreview = $event.target.files.length > 0 ? URL.createObjectURL($event.target.files[0]) : '';
                        document.getElementById('logo-name').textContent = $event.target.files[0].name;">
                    <img x-show="logoPreview" :src="logoPreview" class="mt-2" style="max-width: 200px; max-height: 200px;">
                </label>
                <span id="logo-name" class="text-sm text-gray-600" x-text="logoPreview ? '' : 'ロゴがアップロードされていません。'"></span>
            </div>
            <x-input-error :messages="$errors->get('logo')" class="mt-1" />
        </div>
    </div>
```

![スクリーンショット 2024-03-23 23.24.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3012370/12c15d80-a38d-dc36-ad2f-70a861518c06.png)