intertia.js + filepond + spatie media lib決定版(4) 実際のファイル編集処理を書く
以上を踏まえて実際の実装を行う
まず既存のupdateのためのseeder
postテーブルのseedingをやった時に一々全部消えるのはあんまイケてないので、なんとなくseederを作っておくと開発が便利になるだろう。
% ls database/seeders
DatabaseSeeder.php PostSeeder.php
ここまでの記事の指示通りに作っていたらPostSeederとPostFactoryがあるはずなので、それを改良する
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);
テストしてみよう。既存のファイルを消さなかったとき
既存のファイルを消しこんだとき
あとはこれに基いて消しこめば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には関係ないので本編はここで終わり。重には編集時のファイルのリスティングについて伸べました。ではでは。
この記事が気に入ったらサポートをしてみませんか?