Setiap fitur yang menulis berkas lambat laun menjadi sumber sampah: hasil eksport yang diunduh sekali, thumbnail usang milik berkas yang sudah diganti, folder tmp yang membengkak tiap hari. Database punya migration dan seeder untuk tumbuh teratur, tetapi filesystem jarang mendapat perawatan setara. Chapter ini menutup section File Storage dengan rutinitas kebersihan: Storage::delete() dalam segala bentuknya, sinkronisasi model-berkas lewat observer, pemangkasan berdasarkan umur berkas, dan penjadwalannya agar berjalan sendiri tiap malam.
delete: Satu Berkas atau Sekaligus #
Storage::delete() menerima satu path string atau array of path — keduanya kita buktikan pada folder tmp:
use Illuminate\Support\Facades\Storage;
$disk = Storage::disk('public');
$disk->put('tmp/berkas-a.txt', 'a');
$disk->put('tmp/berkas-b.txt', 'b');
$disk->put('tmp/berkas-c.txt', 'c');
$disk->delete('tmp/berkas-a.txt'); // satu berkas
$disk->delete(['tmp/berkas-b.txt', 'tmp/berkas-c.txt']); // beberapa sekaligus
$sisa = $disk->files('tmp');
echo 'sisa di tmp setelah delete: '.(count($sisa) > 0 ? implode(', ', $sisa) : '(kosong)')."\n";Output yang Dihasilkan:
sisa di tmp setelah delete: (kosong)Menghapus berkas yang tidak ada bukan error — delete() hanya mengembalikan false — sehingga rutinitas cleanup tidak perlu berhenti karena satu berkas hilang duluan. Untuk membereskannya sampai ke foldernya, deleteDirectory() menghapus folder beserta seluruh isinya:
$disk->put('tmp/laporan-hari-ini.txt', 'masih segar');
$disk->makeDirectory('tmp/cache');
$disk->put('tmp/cache/fragmen-1.txt', 'fragmen');
$disk->deleteDirectory('tmp/cache');
echo 'folder tmp/cache masih ada: '.var_export($disk->directoryExists('tmp/cache'), true)."\n";
echo 'tmp/cache/fragmen-1.txt masih ada: '.var_export($disk->exists('tmp/cache/fragmen-1.txt'), true)."\n";Output yang Dihasilkan:
folder tmp/cache masih ada: false
tmp/cache/fragmen-1.txt masih ada: falseSinkron Model dan Berkas: Observer #
Penghapusan paling rawan justru yang paling rutin: baris model dihapus dari database, berberkas fisiknya tertinggal menjadi orphan. Solusinya adalah observer yang mendengarkan event deleted lalu menghapus berkasnya:
namespace App\Observers;
use App\Models\Dokumen;
use Illuminate\Support\Facades\Storage;
class DokumenObserver
{
/**
* Hapus berkas fisik ketika baris model dihapus.
*/
public function deleted(Dokumen $dokumen): void
{
Storage::delete($dokumen->path);
}
}Registrasinya satu baris di boot() milik AppServiceProvider:
use App\Models\Dokumen;
use App\Observers\DokumenObserver;
Dokumen::observe(DokumenObserver::class);Pattern observer untuk model Eloquent secara umum sudah kita bahas di chapter Observers
; di sini event deleted menjadi pemicunya. Feature test berikut membuktikan sinkronnya:
public function test_menghapus_model_juga_menghapus_berkasnya(): void
{
Storage::fake('local');
$budi = User::factory()->create(['id' => 1, 'name' => 'Budi']);
Storage::disk('local')->put('users/1/faktur-2024.txt', 'isi faktur Budi');
$dokumen = Dokumen::create([
'user_id' => $budi->id,
'judul' => 'Faktur Januari 2024',
'path' => 'users/1/faktur-2024.txt',
]);
Storage::disk('local')->assertExists('users/1/faktur-2024.txt');
$dokumen->delete();
$this->assertDatabaseMissing('dokumen', ['id' => $dokumen->id]);
Storage::disk('local')->assertMissing('users/1/faktur-2024.txt');
}Output yang Dihasilkan:
PASS Tests\Feature\FileCleanupTest
✓ menghapus model juga menghapus berkasnya 0.15s
Tests: 1 passed (3 assertions)
Duration: 0.22sPemangkasan Berdasarkan Umur #
Untuk folder sampah seperti tmp, kriteria yang paling jujur adalah waktu modifikasi terakhir. lastModified() mengembalikan Unix timestamp, dan touch() di runner ini dipakai untuk memundurkan waktu dua berkas ke tahun 2023 seolah sudah lama tidak tersentuh:
$disk->put('tmp/log-2023.txt', 'log lama');
$disk->put('tmp/log-baru.txt', 'log baru');
$disk->put('tmp/laporan-hari-ini.txt', 'masih segar');
// mundurkan waktu modifikasi dua berkas ke tahun 2023
touch($disk->path('tmp/log-2023.txt'), strtotime('2023-06-15 08:00:00'));
touch($disk->path('tmp/log-baru.txt'), strtotime('2023-12-31 23:59:59'));
// berkas yang terakhir dimodifikasi lebih dari 30 hari lalu → hapus
$batas = now()->subDays(30)->getTimestamp();
$dihapus = [];
foreach ($disk->files('tmp') as $berkas) {
if ($disk->lastModified($berkas) < $batas) {
$disk->delete($berkas);
$dihapus[] = $berkas;
}
}
echo 'terhapus (lebih tua dari 30 hari): '.implode(', ', $dihapus)."\n";
echo 'tersisa di tmp: '.implode(', ', $disk->files('tmp'))."\n";Output yang Dihasilkan:
terhapus (lebih tua dari 30 hari): tmp/log-2023.txt, tmp/log-baru.txt
tersisa di tmp: tmp/laporan-hari-ini.txtDua berkas tua terpangkas, satu berkas hari ini selamat — persis perilaku yang diinginkan dari kebijakan retensi.
Command Terjadwal untuk Produksi #
Logika pemangkasan tadi layak menjadi Artisan command agar bisa diuji sendiri dan dijadwalkan. Class BersihkanBerkasSementara di app/Console/Commands menerima opsi --hari sebagai batas umur:
public function handle(): int
{
$batas = now()->subDays((int) $this->option('hari'))->getTimestamp();
$dihapus = 0;
foreach (Storage::disk('public')->files('tmp') as $berkas) {
if (Storage::disk('public')->lastModified($berkas) < $batas) {
Storage::disk('public')->delete($berkas);
$dihapus++;
}
}
$this->info("{$dihapus} berkas sementara dihapus.");
return self::SUCCESS;
}Dengan dua berkas uji — satu berumur 10 hari, satu buatan hari ini — hasilnya nyata:
php artisan dokumen:bersihkanOutput yang Dihasilkan:
1 berkas sementara dihapus.Terakhir, jadwalkan di routes/console.php agar berjalan otomatis setiap malam:
use Illuminate\Support\Facades\Schedule;
Schedule::command('dokumen:bersihkan --hari=7')->dailyAt('02:00');php artisan schedule:listOutput yang Dihasilkan:
0 2 * * * php artisan dokumen:bersihkan --hari=7 Next Due: 6 hours from nowEkspresi cron 0 2 * * * berarti tiap pukul 02:00, dan kolom “Next Due” menyesuaikan jam saat schedule:list dijalankan. Di server produksi, satu entri cron php artisan schedule:run per menit akan mengeksekusi jadwal ini — detail lengkapnya di dokumentasi Scheduling
.
Deteksi Orphan #
Satu konsep penutup yang layak dicatat: orphan — berkas di disk yang tidak dirujuk baris mana pun di database. Observer mencegah orphan baru, tetapi orphan lama (dari era sebelum observer dipasang) hanya bisa ditemukan dengan pembandingan: kumpulkan semua path dari database, bandingkan dengan hasil allFiles(), lalu proses selisihnya. Pembandingan semacam ini sebaiknya jadi command terjadwal kedua yang hanya melapor lebih dulu — menghapus otomatis setelah selisihnya diverifikasi beberapa kali.
Dengan rutinitas bersih ini, siklus hidup berkas di aplikasi kita sudah tertutup penuh: dikonfigurasi, disimpan dengan aman, disajikan dengan izin yang tepat, dibagikan dengan masa berlaku, dan pada akhirnya dimusnahkan dengan tertib. Seluruh API yang dipakai di sepanjang section ini terdokumentasi resmi di dokumentasi Filesystem .