feat: remove unused media collection clearing in destroy methods and add CleanupOrphanedMediaJob for orphaned media management

This commit is contained in:
Yoga Pangestu 2026-08-13 00:39:46 +07:00
parent e1a0d25c3c
commit 1f41f04199
12 changed files with 98 additions and 13 deletions

View File

@ -0,0 +1,96 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
class CleanupOrphanedMediaJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $chunkSize = 100;
public function handle(): void
{
$orphanedMedia = $this->getOrphanedMedia();
if ($orphanedMedia->isEmpty()) {
Log::info('CleanupOrphanedMediaJob: Tidak ada media orphaned.');
return;
}
$totalDeleted = 0;
$orphanedMedia->each(function (Media $media) use (&$totalDeleted) {
$this->deleteMedia($media);
$totalDeleted++;
});
Log::info("CleanupOrphanedMediaJob: Berhasil hapus {$totalDeleted} media orphaned.");
}
private function getOrphanedMedia()
{
$allMedia = Media::select(['id', 'model_type', 'model_id', 'file_name', 'custom_properties', 'disk'])
->get()
->groupBy(fn (Media $m) => $m->model_type.'|'.$m->model_id);
$orphanedIds = [];
foreach ($allMedia as $key => $mediaItems) {
[$modelType, $modelId] = explode('|', $key);
if ($this->isOrphaned($modelType, (int) $modelId)) {
foreach ($mediaItems as $media) {
$orphanedIds[] = $media->id;
}
}
}
if (empty($orphanedIds)) {
return collect();
}
return Media::whereIn('id', $orphanedIds)->get();
}
private function isOrphaned(string $modelType, int $modelId): bool
{
if (! class_exists($modelType)) {
return true;
}
$model = $modelType::withTrashed()->find($modelId);
if (! $model) {
return true;
}
if (method_exists($model, 'trashed') && $model->trashed()) {
return $model->deleted_at->lt(now()->subMonth());
}
return false;
}
private function deleteMedia(Media $media): void
{
$s3Key = $media->getCustomProperty('s3_key');
if ($s3Key && Storage::disk($media->disk)->exists($s3Key)) {
Storage::disk($media->disk)->delete($s3Key);
}
$media->forceDelete();
}
}

View File

@ -168,8 +168,6 @@ public function destroy(CashTransaction $transaction): bool
Cache::forget("cash_transaction_receipt_{$media->id}");
}
$transaction->clearMediaCollection('photos');
$deleted = $transaction->delete();
if ($deleted) {

View File

@ -130,8 +130,6 @@ public function destroy(Expense $expense): bool
Cache::forget("expense_receipt_{$media->id}");
}
$expense->clearMediaCollection('photos');
$deleted = $expense->delete();
if ($deleted) {

View File

@ -357,7 +357,6 @@ public function destroy(Cutting $cutting): bool
}
}
$cutting->clearMediaCollection('images');
$cutting->cuttingResults()->delete();
$cutting->cuttingMaterials()->delete();
$cutting->cuttingMaterialCombinations()->delete();

View File

@ -480,7 +480,6 @@ public function destroy(Purchase $purchase): bool
}
});
$purchase->clearMediaCollection('photos');
$purchase->purchaseItems()->delete();
$purchase->delete();

View File

@ -144,7 +144,6 @@ public function destroy(Restock $restock): bool
});
$restock->restockItems()->delete();
$restock->clearMediaCollection('photos');
$restock->delete();
return true;

View File

@ -299,7 +299,6 @@ public function destroy(Order $order): bool
});
$order->orderItems()->delete();
$order->clearMediaCollection('photos');
$order->delete();
return true;

View File

@ -404,7 +404,6 @@ public function destroy(Product $product): bool
$result = DB::transaction(function () use ($product) {
$product->productVariants->each(function (ProductVariant $variant) {
$variant->productPrices()->delete();
$variant->clearMediaCollection('images');
$variant->delete();
});

View File

@ -156,7 +156,6 @@ public function destroy(Product $product, ProductVariant $variant): bool
$result = DB::transaction(function () use ($variant) {
$variant->productPrices()->delete();
$variant->clearMediaCollection('images');
return $variant->delete();
});

View File

@ -213,7 +213,6 @@ public function destroy(RawMaterial $rawMaterial): bool
{
return DB::transaction(function () use ($rawMaterial) {
$rawMaterial->rawMaterialPrices->each(function (RawMaterialPrice $price) {
$price->clearMediaCollection('images');
$price->delete();
});

View File

@ -93,8 +93,6 @@ public function update(RawMaterialPrice $variant, array $data): RawMaterialPrice
public function destroy(RawMaterial $rawMaterial, RawMaterialPrice $variant): bool
{
$result = DB::transaction(function () use ($variant) {
$variant->clearMediaCollection('images');
return $variant->delete();
});

View File

@ -2,7 +2,9 @@
use App\Console\Commands\GeneratePayrollCommand;
use App\Jobs\CheckAttendancePenaltiesJob;
use App\Jobs\CleanupOrphanedMediaJob;
use Illuminate\Support\Facades\Schedule;
Schedule::command(GeneratePayrollCommand::class)->monthlyOn(1, '00:00');
Schedule::job(new CheckAttendancePenaltiesJob)->dailyAt('00:00');
Schedule::job(new CleanupOrphanedMediaJob)->monthlyOn(1, '01:00');