laravel breeze (react) にavater機能を付ける(2) spatie/laravel-medialibrary
前回まででavatarのファイルを受けとれるようにはなっていると思う。ここではそれを保存するが spatie/laravel-medialibrary を利用する。
spatie/laravel-medialibrary とは
AIの解説で
install
によると今のmainはv11ということである。ただ依存要求がかなり最新めでキツい。とりあえずlaravel10とphp8.2は必要だ。まあ足りなかったらバージョンを下げてみるなりとかして頑張ってみよう。sailの環境ではphp8.2が動いている
% ./vendor/bin/sail php --version
PHP 8.2.12 (cli) (built: Oct 26 2023 17:33:49) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.2.12, Copyright (c) Zend Technologies
with Zend OPcache v8.2.12, Copyright (c), by Zend Technologies
with Xdebug v3.2.1, Copyright (c) 2002-2023, by Derick Rethans
というわけでinstall
composer require "spatie/laravel-medialibrary:^11.0.0"
ちなみに、アタシのreposからcloneしてきた場合はlockファイルが古くなっているので-Wオプションとか付けないと入りません
% ./vendor/bin/sail composer require "spatie/laravel-medialibrary:^11.0.0" -W
spatie/laravel-medialibrary用のテーブル
まあrailsのactive recordを使った事がある人は同じようなもんや。
説明にあるように
artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-migrations"
を行う。そうすると
database/migrations/2024_01_07_012501_create_media_table.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();
});
}
};
まあこれで一元管理するのであるが、artisan migrateなりなんなりでこれを付け加えておくこと。
config
configも引き込んでおく
artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-config"
config/media-library.php が出現する。まあこれはとりあえずデフォルトのままやろう。
そうしたら config/filesystems.php にこのメディア用の設定を追記する
'media' => [
'driver' => 'local',
'root' => public_path('media'),
'url' => env('APP_URL').'/media',
],
これはオフィシャルの通りの設定であるが、public_path とかで外部に向き出しになっておりちょっとアレなので
'media' => [
'driver' => 'local',
'root' => storage_path('app/media'),
'url' => env('APP_URL').'/media',
'visibility' => 'private',
],
こうしておいた。
環境変数のセット
実はこれだけではmediaは使われない。configを見ると
return [
/*
* The disk on which to store added files and derived images by default. Choose
* one or more of the disks you've configured in config/filesystems.php.
*/
'disk_name' => env('MEDIA_DISK', 'public'),
などと書いてありデフォルトではpublicを仕様する設定になっている。つまり環境変数MEDIA_DISKを適切にmediaに変更する。
MEDIA_DISK=media
Userモデルの変更
これが結構大変なのでUserモデルをバッチリ変更しておいて、あとはテンプレ的にこれをコピったりするといいと思う、が、ここでは丁寧に解説しとくよ〜(初回だし)
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
まずはこの3行を加える
そうしたら以下のようにimplementsする
class User extends Authenticatable implements HasMedia
まあ基本これだけなんだけど、ここではavatarってことでthumbnailを150x150と、iconを50x50で保存してみる
public function registerMediaConversions(Media $media = null): void
{
$this->addMediaConversion('thumbnail')
->width(150)
->height(150)
->sharpen(10);
$this->addMediaConversion('icon')
->width(50)
->height(50);
}
一応、全部の内容を以下に示す
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
class User extends Authenticatable implements HasMedia
{
use HasApiTokens, HasFactory, Notifiable, InteractsWithMedia;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
public function registerMediaConversions(Media $media = null): void
{
$this->addMediaConversion('thumbnail')
->width(150)
->height(150)
// ->sharpen(10); // シャープネス
$this->addMediaConversion('icon')
->width(50)
->height(50);
}
}
とはいえ、現代のスマホ文化ではこれだけでは全く足りない事もあるのではあるが、とりあえず最低限こんな感じだろう。シャープネスみたいなのも出来る、とりあえず例として書いておいた。
保存してみる
今のbackendのcontrollerの状態は
public function update(ProfileUpdateRequest $request): RedirectResponse
{
dd($request->all());
$request->user()->fill($request->validated());
if ($request->user()->isDirty('email')) {
$request->user()->email_verified_at = null;
}
$request->user()->save();
return Redirect::route('profile.edit');
}
ここで停止してアップロードが行われたことを確認していたね。ここで
public function update(ProfileUpdateRequest $request): RedirectResponse
{
$request->user()->fill($request->validated());
if ($request->user()->isDirty('email')) {
$request->user()->email_verified_at = null;
}
$request->user()->save();
if ($request->hasFile('avatar')) {
// 既存のアバターを削除し、新しいアバターを追加
$request->user()->clearMediaCollection('avatars');
$request->user()->addMediaFromRequest('avatar')->toMediaCollection('avatars');
}
return Redirect::route('profile.edit');
}
このようにする。ちなみにvalidationはちゃんと行ってないので、適切なメディアがアップロードされるという大前提のコードであるからproductionレベルではProfileUpdateRequest も更新する必要があるだろう。
実行結果
frontendを作りこんでないので微妙にわかり辛いが、このように正しく保存されているかどうかをまず確認する
storage/app/media/1/avatar.png
storage/app/media/1/conversions/avatar-icon.jpg
storage/app/media/1/conversions/avatar-thumbnail.jpg
またmediasというメタテーブルも保存されている
mysql> select * from media\G
*************************** 1. row ***************************
id: 1
model_type: App\Models\User
model_id: 1
uuid: cffd60c7-0cd6-42c4-a0e5-5efdb111050d
collection_name: avatars
name: avatar
file_name: avatar.png
mime_type: image/png
disk: media
conversions_disk: media
size: 448634
manipulations: []
custom_properties: []
generated_conversions: {"icon": true, "thumbnail": true}
responsive_images: []
order_column: 1
created_at: 2024-01-07 12:29:48
updated_at: 2024-01-07 12:29:48
1 row in set (0.00 sec)
tinkerでの確認
とりあえずtinkerのシェルで確認しておこう
> $u = User::find(1)
[!] Aliasing 'User' to 'App\Models\User' for this Tinker session.
= App\Models\User {#6684
id: 1,
name: "Test User",
email: "test@example.com",
email_verified_at: "2024-01-07 12:29:37",
#password: "$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi",
#remember_token: "a6TmpE6aIY",
created_at: "2024-01-07 12:29:37",
updated_at: "2024-01-07 12:29:37",
}
として $u に情報を引いてきたら以下のようにアクセスする。
> $avatar = $u->getMedia('avatars')->first();
= Spatie\MediaLibrary\MediaCollections\Models\Media {#7351
id: 1,
model_type: "App\Models\User",
model_id: 1,
uuid: "cffd60c7-0cd6-42c4-a0e5-5efdb111050d",
collection_name: "avatars",
name: "avatar",
file_name: "avatar.png",
mime_type: "image/png",
disk: "media",
conversions_disk: "media",
size: 448634,
manipulations: "[]",
custom_properties: "[]",
generated_conversions: "{"icon": true, "thumbnail": true}",
responsive_images: "[]",
order_column: 1,
created_at: "2024-01-07 12:29:48",
updated_at: "2024-01-07 12:29:48",
+original_url: "http://localhost/media/1/avatar.png",
+preview_url: "",
}
mediasテーブルと殆ど同じものが出力されてくるだろう。ここで
> $avatar->getPath()
= "/var/www/html/storage/app/media/1/avatar.png"
> $avatar->getPath('thumbnail')
= "/var/www/html/storage/app/media/1/conversions/avatar-thumbnail.jpg"
このような形でアクセスしていく。これに関してはまた次回以降やっていこう。実はこの先も割と面倒な事が待ち受けているぞい。
フロントエンドでアップロードが完了してもpathが残ってる問題
まあこれは気になるかどうかってところだけど
Saved.って出てるのにavatar.pngが残り続けているのが気になる場合
const clearFile = () => {
const fileInput = document.getElementById('avatar');
if (fileInput) {
fileInput.value = '';
}
reset('avatar');
}
とかして
const submit = (e) => {
e.preventDefault();
post(route('profile.update'), { onSuccess: () => clearFile() });
};
とかするといいのかもしれない(いつも放置してるので、あんま自身ない)
次回
当然アップロードされたファイルの表示という事になりますね。お楽しみに。
この記事が気に入ったらサポートをしてみませんか?