optimize:clear Laravel & Filament Tanpa SSH di Shared Hosting

Salah satu hal yang lumrah dialami pengguna Laravel di shared hosting adalah tidak tersedianya akses terminal/SSH. Padahal ada banyak momen dimana perintah php artisan dibutuhkan, misalnya untuk membersihkan cache setelah update kode. Tulisan ini merangkum langkah membuat endpoint khusus di aplikasi Laravel + Filament supaya perintah optimize:clear bisa dijalankan cukup lewat browser, lengkap dengan kendala yang muncul di tengah jalan dan cara mengatasinya.

Masalah: Shared Hosting Tanpa Terminal

Banyak paket shared hosting hanya menyediakan akses lewat cPanel/file manager, tanpa SSH. Ini jadi masalah ketika aplikasi Laravel yang sudah di-deploy ke production memakai cache konfigurasi, route, atau view — begitu ada perubahan kode, cache lama bisa membuat perubahan tidak muncul, atau bahkan membuat aplikasi error, sampai cache-nya dibersihkan.

Kenapa harus di-clear? Laravel menyediakan perintah php artisan optimize untuk men-cache file konfigurasi, event, route, dan view supaya aplikasi lebih cepat di production. Kebalikannya, php artisan optimize:clear menghapus semua file cache tersebut sekaligus data di cache driver default, supaya aplikasi "mulai dari nol" lagi.

Solusi: Endpoint Khusus untuk Clear Cache

Karena tidak ada terminal, solusinya adalah membuat sebuah controller dan route baru di aplikasi Laravel yang, ketika diakses lewat URL tertentu, akan menjalankan perintah artisan dari dalam kode PHP itu sendiri — menggunakan facade Artisan::call() yang memang disediakan Laravel untuk menjalankan perintah artisan secara terprogram.

Controller & Route

Controller barunya (mis. MaintenanceController) memetakan sejumlah "slug" URL yang aman ke perintah artisan aslinya, misalnya slug optimize-clear dipetakan ke perintah optimize:clear. Pemetaan ini sengaja dibatasi hanya untuk perintah yang sifatnya membersihkan/optimasi cache — bukan sembarang perintah artisan seperti migrasi database — supaya endpoint ini tidak berubah jadi celah keamanan kalau ada yang menemukan URL-nya.

Route-nya didaftarkan sebagai GET biasa, misalnya:

/maintenance/{slug}?token=...

Berikut isi lengkap app/Http/Controllers/MaintenanceController.php:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;

class MaintenanceController extends Controller
{
    protected array $commandMap = [
        'optimize-clear' => 'optimize:clear',
        'optimize' => 'optimize',
        'cache-clear' => 'cache:clear',
        'config-clear' => 'config:clear',
        'route-clear' => 'route:clear',
        'view-clear' => 'view:clear',
        'event-clear' => 'event:clear',
        'filament-optimize-clear' => 'filament:optimize-clear',
    ];

    public function run(Request $request, string $command)
    {
        $expectedToken = (string) config('maintenance.token', '');
        $givenToken = (string) $request->query('token', '');

        // Tolak kalau token di config/maintenance.php belum diganti dari
        // nilai default — nilai default itu ditulis terbuka di kode ini,
        // jadi kalau lupa diganti, siapa pun yang baca kode ini otomatis
        // tahu tokennya.
        if ($expectedToken === '' || $expectedToken === 'GANTI-DENGAN-TOKEN-RAHASIA-ANDA') {
            abort(403, 'Token maintenance belum diatur.');
        }

        // hash_equals() dipakai (bukan ===) supaya perbandingan token tahan
        // terhadap timing attack.
        if (! hash_equals($expectedToken, $givenToken)) {
            abort(403, 'Token tidak valid.');
        }

        if ($command === 'clear-all') {
            $log = '';
            foreach (['optimize:clear', 'filament:optimize-clear'] as $artisanCommand) {
                Artisan::call($artisanCommand);
                $log .= "=== php artisan {$artisanCommand} ===\n" . Artisan::output() . "\n";
            }

            return response("OK, semua cache berhasil dibersihkan.\n\n{$log}")
                ->header('Content-Type', 'text/plain; charset=UTF-8');
        }

        if (! array_key_exists($command, $this->commandMap)) {
            abort(404, 'Perintah tidak dikenali.');
        }

        $artisanCommand = $this->commandMap[$command];

        Artisan::call($artisanCommand);
        $output = Artisan::output();

        return response(
            "OK, perintah berhasil dijalankan: php artisan {$artisanCommand}\n\n" .
            "Output:\n" . ($output !== '' ? $output : '(tidak ada output)')
        )->header('Content-Type', 'text/plain; charset=UTF-8');
    }
}

Tambahan di routes/web.php:

use App\Http\Controllers\MaintenanceController;
use Illuminate\Support\Facades\Route;

Route::get('/maintenance/{command}', [MaintenanceController::class, 'run'])
    ->name('maintenance.run');

Proteksi Pakai Token Rahasia

Karena endpoint ini pada dasarnya adalah "pintu belakang" untuk mengeksekusi perintah artisan lewat web, wajib diberi proteksi. Caranya dengan mencocokkan sebuah token rahasia yang dikirim lewat query string dengan token yang tersimpan di sisi server, memakai fungsi hash_equals() supaya perbandingannya aman dari serangan timing attack.

Token ini sengaja disimpan sebagai nilai tetap di file konfigurasi PHP biasa (bukan dibaca lewat fungsi env()), karena kalau file cache konfigurasi (config:cache) sedang bermasalah, Laravel bisa berhenti membaca file .env sama sekali — sehingga nilai token yang bergantung pada env() justru ikut hilang di saat paling dibutuhkan.

Isi config/maintenance.php:

<?php

return [
    'token' => 'GANTI-DENGAN-TOKEN-RAHASIA-ANDA',
];

Token acak bisa dibuat dari komputer lokal dengan cara:

php -r "echo bin2hex(random_bytes(20));"

Atau bisa gunakan generator password atau bebas diisi sendiri yang penting cukup aman.


Kenapa Cache Filament Tidak Ikut Bersih

Jika aplikasi yang di deploy menggunakan Filament sebagai panel admin, ternyata menjalankan optimize:clear saja tidak cukup, karena perintah tersebut murni bawaan Laravel dan tidak menyentuh cache khusus milik Filament.

Filament punya dua jenis cache tambahan di luar cache Laravel:

  • Cache komponen Filament — indeks resource, page, widget, dan komponen Livewire lain, disimpan di folder bootstrap/cache/filament.
  • Cache ikon (Blade Icons) — Filament memakai paket Blade Icons yang performanya jauh lebih baik saat ikon-ikonnya di-cache.

Filament menyediakan perintah php artisan filament:optimize untuk men-cache keduanya sekaligus (gabungan dari filament:cache-components dan icons:cache), dan php artisan filament:optimize-clear untuk membersihkan keduanya sekaligus. Jadi solusinya: endpoint tadi perlu ditambah opsi untuk memanggil filament:optimize-clear juga, idealnya digabung jadi satu slug (mis. clear-all) yang menjalankan optimize:clear dan filament:optimize-clear berurutan dalam satu kali akses.


Kendala: Command Icon Cache Tidak Terdaftar

Begitu filament:optimize-clear dicoba lewat endpoint tadi, muncul error:

Symfony\Component\Console\Exception\NamespaceNotFoundException — "There are no commands defined in the "icons" namespace."

Penyebabnya cukup unik: paket Blade Icons yang dipakai Filament untuk mengelola cache ikon hanya mendaftarkan perintah icons:cache dan icons:clear miliknya ketika aplikasi berjalan dari console/terminal (dicek lewat runningInConsole() di dalam service provider paket tersebut). Karena permintaan datang dari browser (HTTP request biasa), kedua perintah itu dianggap "tidak ada", sehingga proses filament:optimize-clear gagal di tengah jalan saat mencoba membersihkan cache ikon.

Solusinya: daftarkan kedua command tersebut secara manual di method register() pada AppServiceProvider milik aplikasi, memakai method $this->commands([...]) dari Laravel. Dengan begitu, perintah icons:cache dan icons:clear tetap terdaftar dan bisa dipanggil lewat Artisan::call(), sekalipun permintaannya datang dari web, bukan terminal.

Tambahan di app/Providers/AppServiceProvider.php:

public function register(): void
{
    $this->commands([
        \BladeUI\Icons\Console\CacheCommand::class,
        \BladeUI\Icons\Console\ClearCommand::class,
    ]);
}

Setelah pendaftaran manual ini ditambahkan, endpoint clear-all baru bisa berjalan mulus membersihkan cache Laravel maupun cache Filament (komponen + ikon) sekaligus, tanpa perlu SSH sama sekali.

Contoh Penggunaan

Semua slug diakses lewat pola URL yang sama: /maintenance/{slug}?token=TOKEN_RAHASIA. Tinggal ganti bagian {slug} sesuai kebutuhan.

Bersihkan semuanya sekaligus (Laravel + Filament, ini yang paling sering dipakai):

https://domainkamu.com/maintenance/clear-all?token=TOKEN_RAHASIA

Hanya butuh bersihkan cache data aplikasi (mis. cache dari Cache::remember(), bukan config/route):

https://domainkamu.com/maintenance/cache-clear?token=TOKEN_RAHASIA

Hanya cache konfigurasi — dipakai kalau baru mengubah nilai di file config/*.php atau .env:

https://domainkamu.com/maintenance/config-clear?token=TOKEN_RAHASIA

Hanya cache route — dipakai kalau baru menambah/mengubah route:

https://domainkamu.com/maintenance/route-clear?token=TOKEN_RAHASIA

Hanya cache view (Blade) — dipakai kalau baru mengubah file .blade.php tapi perubahannya belum muncul:

https://domainkamu.com/maintenance/view-clear?token=TOKEN_RAHASIA

Semua cache bawaan Laravel sekaligus (setara optimize:clear, tanpa menyentuh cache Filament):

https://domainkamu.com/maintenance/optimize-clear?token=TOKEN_RAHASIA

Hanya cache Filament (komponen + ikon) — dipakai kalau baru menambah resource/page/widget Filament baru:

https://domainkamu.com/maintenance/filament-optimize-clear?token=TOKEN_RAHASIA
Kalau slug atau token salah, endpoint akan merespons error (403 untuk token salah, 404 untuk slug yang tidak dikenali) — bukan diam-diam menjalankan sesuatu yang tidak diminta. Kalau berhasil, responsnya berupa teks biasa berisi output dari perintah artisan yang dijalankan, jadi bisa langsung dilihat di browser apakah prosesnya sukses.

Kesimpulan

Ada tiga poin penting dari kasus ini bagi siapa pun yang mengelola aplikasi Laravel/Filament di shared hosting tanpa akses terminal:

  1. Perintah artisan apa pun bisa dijalankan secara terprogram dari dalam kode lewat Artisan::call(), sehingga bisa "dibungkus" jadi endpoint web yang diproteksi token.
  2. optimize:clear hanya membersihkan cache bawaan Laravel — kalau memakai Filament, cache komponen dan ikonnya perlu dibersihkan terpisah lewat filament:optimize-clear.
  3. Sebagian command pihak ketiga (seperti command milik paket Blade Icons) sengaja hanya terdaftar saat berjalan dari console, sehingga kalau ingin dipanggil dari luar terminal (web/queue/scheduler), command tersebut perlu didaftarkan manual di service provider aplikasi.

Referensi

Posting Komentar