見出し画像

intertia.js + filepond + spatie media lib決定版(4) 実際のファイル編集処理を書く

以上を踏まえて実際の実装を行う

まず既存のupdateのためのseeder

postテーブルのseedingをやった時に一々全部消えるのはあんまイケてないので、なんとなくseederを作っておくと開発が便利になるだろう。

% ls database/seeders
DatabaseSeeder.php  PostSeeder.php

ここまでの記事の指示通りに作っていたらPostSeederPostFactoryがあるはずなので、それを改良する

database/factories/PostFactory.php 

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Post>
 */
class PostFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'content' => fake()->paragraph,
        ];
    }
}

database/seeders/PostSeeder.php 

<?php

namespace Database\Seeders;

use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

use App\Models\Post;

class PostSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        Post::factory()->count(3)->create();
    }
}

database/seeders/DatabaseSeeder.php 

<?php

namespace Database\Seeders;

use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        // User::factory(10)->create();

        User::factory()->create([
            'name' => 'Test User',
            'email' => 'test@example.com',
        ]);
        $this->call([
            PostSeeder::class,
        ]);
    }
}

これでseedすると

ダミーデーターが構築された。しかし添付はまだない。

こんな感じになる。fakerのlocaleをセットしてないので英語が適当だけどまあこれは本題じゃないからいいだろう。

ファイルを添付する

今のseedはpostを3つ作っただけで何らseedできていないので、seedする

database/seeders/PostSeeder.php 

<?php

namespace Database\Seeders;

use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

use App\Models\Post;

class PostSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        Post::factory()->count(3)->create()->each(function ($post) {
            $post->addMediaFromUrl('https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png')->toMediaCollection('files');
        });
    }
}

とりあえずgoogleのロゴを直接リンクしちゃったんで変えるならこれは適当に変更してほしい。ローカルpathも指定できるけど移動しちゃうからね。seedの状態に戻して開発するには面倒なので、そこは気をつけて。

seedに基いてeditのformを正しく書く

まず、inertiajs+ reactは事前ロードしておかないとリレーション読めないのでやっておく

app/Http/Controllers/PostController.php 

    public function edit(Post $post): Response
    {
        $post->load('media'); // これ
        return Inertia::render('Posts/Edit', [
            'targetedPost' => $post,
        ]);
    }

この情報をfilepondに渡していく

resources/js/Pages/Posts/Edit.jsx 

  const [files, setFiles] = useState(
    targetedPost.media.map(media => ({
      source: media.id,
      options: {
        type: 'local',
        file: {
          id: media.id,
          name: media.file_name,
          type: media.mime_type,
          size: media.size,
        },
        metadata: {
        },
      },
    }))
  );

そうすると

こんな感じになる。

poster表示用のroute

基本的にはこれを使ったらいい

Route::get('/posts/{post}/{media}/download', [PostController::class, 'download'])->name('posts.download');

これに従ってposterのリンクをセットする

  const [files, setFiles] = useState(
    targetedPost.media.map(media => ({
      source: media.id,
      options: {
        type: 'local',
        file: {
          id: media.id,
          name: media.file_name,
          type: media.mime_type,
          size: media.size,
        },
        metadata: {
          poster: route("posts.download", [targetedPost.id, media.id]),
        },
      },
    }))
  );

そうすればまともにpreviewが表示されるはずだ

すげえ横長だけどね

デザインの修正

ここで追加のファイルを与えると

デザインがやばい

こんな風になって都合が悪い。この時点でデザインを修正しておく。

                <div className="mt-4 filepond-container">
                  <FilePond
                    files={files}
                    onupdatefiles={setFiles}
                    allowMultiple={true}
                    maxFiles={4}
                    name="files"
                    labelIdle='Drag & Drop your files or <span class="filepond--label-action">Browse</span>'
                    className="filepond--root"
                  />
                </div>

このようにfilepond-container と filepond--root というclassを与えた。そして

      <style>
        {`
          .filepond--root {
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
          }

          .filepond--item {
            width: calc(25% - 10px);
          }
        `}
      </style>
    </AuthenticatedLayout>
  );
}

こんな感じで末尾に定義すると

こんな感じになる。

埋まってますけどね

こんな感じで追加していくことができる。縦のサイズが十分にないやつがinitialであるとメリこんで気持ちわるいとかいう場合は頑張ってデザインを探ってみてください。ま、これはchatgptが出力してきたやつです。

updateの処理

これは結構複雑で

  • 新規追加は問答無用で追加

  • 既存のものは現在あるファイルIDと比較して

    • 存在しているIDはスルー

    • 存在してなければ削除

みたいな処理をしないといけない。ここでまず一番簡単なのは新規のファイルを問答無用で追記していくことである。

追記する

まず現在のupdateであるが

    public function update(Request $request, Post $post): RedirectResponse
    {
        $post->update($request->all());
        return redirect(route('posts.index'))
            ->with('success', 'Updated');
    }

このようにシンプル極まりないものになっているから、まずこれをちょっと改造する。

    public function update(Request $request, Post $post): RedirectResponse
    {
        DB::beginTransaction();
        $data = $request->all();
        $post->update($request->all());
        $files = $data['files'] ?? [];
        if ($files) {
            foreach ($files as $file) {
            }
        }

        return redirect(route('posts.index'))
            ->with('success', 'Updated');
    }

中途半端なトランザクションでfiles配列をぶん回している。今、既存のファイルと新規を混同してuploadしてみると

array:2 [▼ // app/Http/Controllers/PostController.php:94
  0 => array:3 [▶]
  1 => Illuminate\Http\UploadedFile {#1330 ▶}
]

こんな感じになる。つまり配列部分はスルーして Illuminate\Http\UploadedFile 的なobjectの場合のみ処理すればいいって事になりますね。それを踏まえてちょいとテストコードを書いてみると

use Illuminate\Http\UploadedFile;
//
    public function update(Request $request, Post $post): RedirectResponse
    {
        DB::beginTransaction();
        $data = $request->all();
        $post->update($request->all());
        $files = $data['files'] ?? [];
        if ($files) {
            foreach ($files as $file) {
                if ($file instanceof UploadedFile) {
                    dump($file);
                } 
            }
        }
        die("test");


if ($file instanceof UploadedFile) {
    dump($file);
} 

これでUploadedFileだった時のみ処理するってことで新規upload分が取れるので、これを追記していけばいいね。

既存のファイルと現在のファイルの比較

現在はダミーのシーダーで1つファイルがuploadされている状態だが、それをそのままにしたり消したりしてみよう。

少なくとも、そのままのときはそのまま。消えてたときは添付ファイルを削除せねばならない。ということはまず既存のファイル一覧を取り出しておく。

    public function update(Request $request, Post $post): RedirectResponse
    {
        DB::beginTransaction();
        $data = $request->all();
        $post->update($request->all());
        $files = $data['files'] ?? [];

        $existingFiles = $post->media->keyBy('id')->all();
        dd($existingFiles);

これによりIDをキーとしてファイル一覧が組み直された。

そしたら $uploadedMediaIds にアップロード済みIDを格納する。ただし本来はここでもobjectかどうか確認が必要だが。

    public function update(Request $request, Post $post): RedirectResponse
    {
        DB::beginTransaction();
        $data = $request->all();
        $post->update($request->all());
        $files = $data['files'] ?? [];
        $existingFiles = $post->media->keyBy('id')->all();
        $uploadedMediaIds= [];
        foreach ($files as $file) {
            $uploadedMediaIds[$file['id']] = null;
        }
        dump($uploadedMediaIds);
        dd($existingFiles);

こんな風にしてpostされたidを取れば

これで比較可能になるのでこのdiffが利用されなかったもの=削除対象になるはずだ

    public function update(Request $request, Post $post): RedirectResponse
    {
        DB::beginTransaction();
        $data = $request->all();
        $post->update($request->all());
        $files = $data['files'] ?? [];
        $existingFiles = $post->media->keyBy('id')->all();
        $uploadedMediaIds= [];
        foreach ($files as $file) {
            $uploadedMediaIds[$file['id']] = null;
        }
        $unusedFiles = array_diff_key($existingFiles, $uploadedMediaIds);
        dd($unusedFiles);

テストしてみよう。既存のファイルを消さなかったとき

削除対象は空である

既存のファイルを消しこんだとき

削除対象がunusedに入っている

あとはこれに基いて消しこめばok

    public function update(Request $request, Post $post): RedirectResponse
    {
        DB::beginTransaction();
        $data = $request->all();
        $post->update($request->all());
        $files = $data['files'] ?? [];
        $existingFiles = $post->media->keyBy('id')->all();
        $uploadedMediaIds= [];
        foreach ($files as $file) {
            $uploadedMediaIds[$file['id']] = null;
        }
        $unusedFiles = array_diff_key($existingFiles, $uploadedMediaIds);
        foreach ($unusedFiles as $unusedFile) {
            $unusedFile->delete();
        }
        if ($files) {
            foreach ($files as $file) {
                if ($file instanceof UploadedFile) {
                    $post->addMedia($file)->toMediaCollection('files');
                }
            }
        }
        DB::commit();

        return redirect(route('posts.index'))
            ->with('success', 'Updated');
    }

リファクタリング

相変わらずvalidationは面倒見ていないので、必要なら適時書く事

    public function update(Request $request, Post $post): RedirectResponse
    {
        DB::beginTransaction();

        try {
            $data = $request->all();
            $post->update([
                'content' =>$data['content'],
            ]);

            $files = $data['files'];
            if ($files) {
                // アップロード済みのファイルIDを格納する配列
                $uploadedMediaIds = [];
                $existingMedia = $post->media->keyBy('id')->all();
                foreach ($files as $file) {
                    if ($file instanceof UploadedFile) {
                        // 新しくuploadされたファイルを保存
                        $post->addMedia($file)->toMediaCollection('files');
                    } else {
                        // IDをリスト
                        $uploadedMediaIds[$file['id']] = null;
                    }
                }
                $unusedMedia = array_diff_key($existingMedia, $uploadedMediaIds);

                // 使用されていないメディアを削除
                collect($unusedMedia)->each(function ($media) {
                    $media->delete();
                });

            }
            DB::commit();
            return redirect(route('posts.index'))
                ->with('success', 'Post successfully updated.');

        } catch (\Exception $e) {
            DB::rollBack();
            return redirect()->back()->withErrors('Failed to update post.');
        }
    }

細かくみていこう。まず大きな変更はtryブロックを仕掛けて失敗した時にrollebackをしていること。

続いて

            $data = $request->all();
            $post->update([
                'content' =>$data['content'],
            ]);

具体的なキーのみupdateしている。最終的にはvalidationに回す

続いて

            $files = $data['files'];
            if ($files) {
// ...

filesがある時だけ処理

                foreach ($files as $file) {
                    if ($file instanceof UploadedFile) {
                        // 新しくuploadされたファイルを保存
                        $post->addMedia($file)->toMediaCollection('files');
                    } else {
                        // IDをリスト
                        $uploadedMediaIds[$file['id']] = null;
                    }
                }
                $unusedMedia = array_diff_key($existingMedia, $uploadedMediaIds);

新規ファイルの追加とlistへの書き出しを同時に行っている。最終的に$unusedMediaとして利用していないファイルを洗い出す

続いて

// 使用されていないメディアを削除
$unusedMedia = array_diff_key($existingMedia, $uploadedMediaIds);
collect($unusedMedia)->each(function ($media) {   
    $media->delete();
});

これは

        foreach ($unusedFiles as $unusedFile) {
            $unusedFile->delete();
        }

と同じだけど何となく別の書き方をしてみた。ここは別にforeachでいいすよ

というわけで

validationは未完了だけどfilepondには関係ないので本編はここで終わり。重には編集時のファイルのリスティングについて伸べました。ではでは。





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