Arsitektur Service & Manajemen Foto
Pemisahan logika bisnis penyimpanan destinasi dan pengelolaan berkas fisik gambar ke dalam lapisan Service khusus.
Membaca sekitar 2 menit
Pemisahan logika bisnis penyimpanan destinasi dan pengelolaan berkas fisik gambar ke dalam lapisan Service khusus.
DestinationService dan SubmissionService mendelegasikan urusan unggah, penggantian sampul (hero), pengurutan galeri, hingga penghapusan file fisik ke PhotoService.
Menghapus baris database tanpa menghapus file di storage akan menumpuk sampah disk; membungkusnya di satu service menjamin sinkronisasi data dan file.
Masalahnya dulu
Setiap destinasi wisata memiliki dua jenis kebutuhan visual yang berbeda: satu foto utama (hero) sebagai wajah di kartu katalog dan banner atas, serta kumpulan foto tambahan (gallery) yang dibuka menggunakan GLightbox. Jika logika unggah file, penomoran urutan, dan penghapusan file lama ditaruh langsung di dalam Controller, kode yang sama akan terduplikasi antara form publik dan form admin.
Sentralisasi operasi berkas di PhotoService
Aplikasi memisahkan pengelolaan gambar ke dalam PhotoService yang di-inject ke DestinationService maupun SubmissionService. Tabel photos menggunakan kolom type ('hero' atau 'gallery') dan order. Saat admin mengganti foto hero, PhotoService otomatis menghapus file fisik lama di disk public beserta record databasenya sebelum menyimpan foto baru. Sementara untuk galeri, foto baru ditambahkan secara inkremental melanjutkan urutan terakhir yang sudah ada.
Menghapus baris di tabel database itu mudah, tetapi memastikan tidak ada file yatim (orphan files) yang tertinggal di ruang penyimpanan membutuhkan disiplin di lapisan service.
Implementasi di ngodingan ini
Implementasi PhotoService.php yang menangani siklus hidup foto hero, penambahan galeri berurutan, dan pembersihan menyeluruh saat destinasi dihapus:
class PhotoService {
public function storeHero(Destination $destination, $file): void {
if ($destination->hero) {
Storage::disk('public')->delete($destination->hero->path);
$destination->hero->delete();
}
$path = $file->store('destinations', 'public');
$destination->photos()->create([
'path' => $path, 'is_cover' => true, 'type' => 'hero', 'order' => 0,
]);
}
public function storeGallery(Destination $destination, array $files): void {
$startOrder = $destination->gallery()->count();
foreach ($files as $index => $file) {
$path = $file->store('destinations', 'public');
$destination->photos()->create([
'path' => $path, 'is_cover' => false, 'type' => 'gallery',
'order' => $startOrder + $index,
]);
}
}
}Jebakan dan detail yang harus dijaga
- Meskipun migrasi tabel photos sudah memakai cascadeOnDelete(), fitur cascade milik database hanya menghapus baris tabel dan tidak menyentuh sistem file OS. Karena itu DestinationService::delete() wajib memanggil photoService->deleteAll($destination) sebelum mengeksekusi $destination->delete().
- Di model Destination, relasi hero() menggunakan hasOne()->where('type', 'hero') sedangkan gallery() menggunakan hasMany()->where('type', 'gallery'). Pemisahan relasi ini memudahkan eager loading selektif seperti Destination::with('hero') di halaman katalog tanpa perlu menarik seluruh foto galeri.