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(); } }