画像保存ライブラリにlaravel-medialibraryを使用しました。
Illuminate\Da
導入
composer require "spatie/laravel-medialibrary:^11.0.0"
php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider"
php artisan migrate
以下のマイグレーションファイルが作成されます。
<?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();
});
}
};
メディアをモデルに関連付けるには、モデルが次のインターフェイスと特性を実装する必要があります。
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Spatie\MediaLibrary\HasMedia;//追加
use Spatie\MediaLibrary\InteractsWithMedia; //追加
class User extends Model implements HasMedia//追加
{
use InteractsWithMedia;//追加
}
カスタムパスジェネレータを作成
デフォルトでは、ファイルはメディア オブジェクトの ID を名前として使用するディレクトリ内に保存されます。
media
---- 1
------ file.jpg
------ conversions
--------- small.jpg
--------- medium.jpg
--------- big.jpg
ですが、モデル名/id/logs/ファイル名
にしたかったので保存先をカスタマイズしました。
カスタム パス ジェネレーターを作成します。
getPath
メソッドで保存先のディレクトリを指定しています。
PathGenerator
インターフェースをオーバーライドした、CustomPathGenerator
クラスを作成します。
<?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 キーの値をカスタムパスジェネレータクラスに設定します。
'path_generator' => App\PathGenerators\CustomPathGenerator::class,
user
---- 1
------ logs
--------- small.jpg
---- 2
------ logs
--------- sample.jpg
モデルのidごとに任意のディレクトリ名(この場合はlogs
)で保存されるようになります。
コントローラー保存処理
全体のコード
public function update(Request $request)
{
//validation処理省略
if ($request->hasFile('logo')) {
// 既存のロゴ画像を削除して新しい画像を保存
$company->clearMediaCollection('logos');
$company->addMediaFromRequest('logo')->toMediaCollection('logos');
}
//リダイレクト処理
}
今回はプロフィールにロゴを持たせたので、更新処理に記述しました。
工夫した点は、このままだと既存の画像をファイルに残したまま新しく画像を追加してしまい、ストレージを余分に使用してしまいます。
if ($request->hasFile('logo')) {
$company->addMediaFromRequest('logo')->toMediaCollection('logos');
}
無駄なストレージを使うのを防ぐために下記を追加して、一旦全て削除し上書きされるようにしました。
$company->clearMediaCollection('logos');
viewに表示
<img src="{{ $company->getFirstMediaUrl('logos') }}">
Alpine.jsでファイル選択時にプレビュー
Alpine.jsを導入してある前提です。
通常はファイル選択しても、画像は表示されずファイル名のみが表示されます。
画像機能としては果たしていますが、選択した画像が表示される方がUXがいいと思い実装しました。
また、tailwindcssでレイアウトをしました。
まず、logoPreview
を定義します。input type=file
がクリックされると@change
によって画像のURLを取得するロジックが発火され、logoPreview
に画像のURLが入ります。
//画像がある場合は初期値にその画像の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>