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:

php
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:

text
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:

php
$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:

text
folder tmp/cache masih ada: false
tmp/cache/fragmen-1.txt masih ada: false

Sinkron 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:

php
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:

php
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:

php
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:

text
   PASS  Tests\Feature\FileCleanupTest
  ✓ menghapus model juga menghapus berkasnya                             0.15s

  Tests:    1 passed (3 assertions)
  Duration: 0.22s

Pemangkasan 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:

php
$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:

text
terhapus (lebih tua dari 30 hari): tmp/log-2023.txt, tmp/log-baru.txt
tersisa di tmp: tmp/laporan-hari-ini.txt

Dua 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:

php
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:

bash
php artisan dokumen:bersihkan

Output yang Dihasilkan:

text
1 berkas sementara dihapus.

Terakhir, jadwalkan di routes/console.php agar berjalan otomatis setiap malam:

php
use Illuminate\Support\Facades\Schedule;

Schedule::command('dokumen:bersihkan --hari=7')->dailyAt('02:00');
bash
php artisan schedule:list

Output yang Dihasilkan:

text
  0 2 * * *  php artisan dokumen:bersihkan --hari=7  Next Due: 6 hours from now

Ekspresi 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 .