見出し画像

laravel breeze (react) にavater機能を付ける(2) spatie/laravel-medialibrary

前回まででavatarのファイルを受けとれるようにはなっていると思う。ここではそれを保存するが spatie/laravel-medialibrary を利用する。


spatie/laravel-medialibrary とは

AIの解説で


install


によると今のmainはv11ということである。ただ依存要求がかなり最新めでキツい。とりあえずlaravel10php8.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() });
  };

とかするといいのかもしれない(いつも放置してるので、あんま自身ない)

次回

当然アップロードされたファイルの表示という事になりますね。お楽しみに。

この記事が気に入ったらサポートをしてみませんか?