Facade Storage menyatukan pembacaan, penulisan, dan penghapusan berkas ke berbagai media penyimpanan di balik satu antarmuka PHP yang sama. Topik ini membahas konsep disk pada aplikasi catatan, operasi dasar pada disk local, penyiapan disk public beserta tautannya ke direktori web, dan penanganan unggahan dari formulir. Setelah membacanya, berkas lampiran dapat disimpan, dibaca kembali, disajikan melalui URL, dan dipindahkan ke media lain hanya dengan mengubah konfigurasi.
Disk pada facade Storage#
Disk adalah konfigurasi satu media penyimpanan yang memuat driver serta lokasi berkasnya. Daftar disk didefinisikan pada berkas config/filesystems.php, sedangkan disk yang digunakan secara default dipilih melalui variabel FILESYSTEM_DISK pada berkas .env. Proyek aplikasi catatan menggunakan nilai local, sehingga pemanggilan metode Storage tanpa nama disk ditujukan ke disk local. Setiap disk menyimpan berkas pada direktori akarnya masing-masing, sebagaimana tabel berikut.
| Disk | Driver | Lokasi berkas | Peruntukan |
|---|---|---|---|
local | local | storage/app/private | berkas privat aplikasi |
public | local | storage/app/public | berkas yang disajikan melalui URL |
s3 | s3 | bucket pada layanan penyimpanan S3 | berkas pada penyimpanan awan |
Disk local dan disk public sama-sama menggunakan driver local karena keduanya menyimpan berkas pada sistem berkas server yang sama, dan perbedaannya hanya ada pada lokasi serta peruntukannya. Disk local menulis ke storage/app/private, direktori yang tidak dilayani web server, sehingga berkas di dalamnya hanya dapat dibaca melalui API Storage. Berkas yang berisi data pribadi pengguna ditempatkan di disk local, sedangkan berkas yang memang harus dapat diminta browser ditempatkan pada disk public.
Contoh berikut menulis satu berkas arsip pada disk local dari Tinker, kemudian membaca kembali ukuran, isi, dan alamat lengkapnya.
# menulis lalu membaca berkas arsip pada disk local
cd ~/aplikasi-catatan
php artisan tinker --execute='
use Illuminate\Support\Facades\Storage;
Storage::disk("local")->delete("arsip-demo.txt");
Storage::disk("local")->put("arsip-demo.txt", "Isi catatan pertama disimpan lewat disk local.\n");
echo "Ada: " . var_export(Storage::disk("local")->exists("arsip-demo.txt"), true) . PHP_EOL;
echo Storage::disk("local")->get("arsip-demo.txt");
echo "Ukuran: " . Storage::disk("local")->size("arsip-demo.txt") . " byte" . PHP_EOL;
echo "Jalur: " . Storage::disk("local")->path("arsip-demo.txt") . PHP_EOL;
'Ada: true
Isi catatan pertama disimpan lewat disk local.
Ukuran: 47 byte
Jalur: /home/kal/aplikasi-catatan/storage/app/private/arsip-demo.txtMetode path() mengembalikan alamat lengkap berkas pada sistem berkas, yang menegaskan bahwa hasil put() adalah berkas biasa dan bukan entri pada database. Pembacaan dengan cat pada alamat yang sama menghasilkan isi yang persis sama, sedangkan metode delete() menghapusnya, sehingga missing() mengembalikan true.
# membaca isi berkas langsung dari sistem berkas, lalu menghapusnya
cd ~/aplikasi-catatan
cat storage/app/private/arsip-demo.txt
php artisan tinker --execute='
use Illuminate\Support\Facades\Storage;
Storage::disk("local")->delete("arsip-demo.txt");
echo "Setelah dihapus: " . var_export(Storage::disk("local")->missing("arsip-demo.txt"), true) . PHP_EOL;
'Isi catatan pertama disimpan lewat disk local.
Setelah dihapus: trueDisk public dan tautan storage#
Berkas pada disk public dapat diminta dari URL setelah tautan simbolik (symlink) dari public/storage menuju storage/app/public dibuat. Direktori public adalah satu-satunya direktori yang dilayani web server, sedangkan storage/app/public berada di luar jangkauannya, sehingga keduanya perlu dihubungkan secara eksplisit. Laravel menyediakan perintah storage:link untuk membuat tautan tersebut.
# membuat tautan public/storage menuju storage/app/public
cd ~/aplikasi-catatan
rm -f public/storage
php artisan storage:link
readlink public/storage INFO The [public/storage] link has been connected to [storage/app/public].
/home/kal/aplikasi-catatan/storage/app/publicBaris INFO menegaskan tautan sudah terhubung, sedangkan readlink menampilkan targetnya, yaitu direktori akar disk public. Setelah tautan ini ada, berkas storage/app/public/lampiran/lampiran-demo.txt dapat diminta melalui URL http://localhost:8000/storage/lampiran/lampiran-demo.txt, dengan awalan URL yang diambil dari konfigurasi url pada definisi disk public.
Menangani unggahan dari formulir#
Berkas yang dikirim dari formulir tiba pada controller sebagai instance UploadedFile, yaitu kelas yang membungkus berkas sementara beserta nama dan ukuran aslinya. Instance tersebut diambil dengan $request->file('lampiran'), divalidasi terlebih dahulu, lalu disimpan ke disk tujuan dengan metode storeAs(). Formulir untuk mengunggahnya adalah sebagai berikut.
<form action="{{ route('lampiran.store') }}" method="POST" enctype="multipart/form-data">
@csrf
<label for="lampiran">Lampiran catatan</label>
<input type="file" name="lampiran" id="lampiran">
@error('lampiran')
<p>{{ $message }}</p>
@enderror
<button type="submit">Unggah</button>
</form>Atribut enctype="multipart/form-data" wajib ada agar isi berkas ikut terkirim, sedangkan direktif @csrf melindungi formulir dari pemalsuan permintaan dan @error menampilkan pesan error validasi. Formulir tersebut dikirim ke rute yang mendaftarkan aksi store pada controller, misalnya Route::post('/lampiran', [LampiranController::class, 'store'])->name('lampiran.store'); pada berkas routes/web.php. Isi controllernya sebagai berikut.
// tidak-dijalankan
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class LampiranController extends Controller
{
/**
* Menyimpan unggahan lampiran pada disk public.
*/
public function store(Request $request)
{
$request->validate([
'lampiran' => ['required', 'file', 'max:512'],
]);
$jalur = $request->file('lampiran')->storeAs('lampiran', 'lampiran-demo.txt', 'public');
return back()->with('status', "Lampiran tersimpan pada {$jalur}.");
}
}Metode store() pada controller memvalidasi masukan dengan aturan required yang menolak unggahan kosong, file yang menolak masukan yang bukan berkas, dan max:512 yang membatasi ukurannya pada 512 kilobita. Setelah lolos, metode storeAs() menempatkan berkas pada direktori lampiran di disk public dengan nama lampiran-demo.txt, lalu mengembalikan jalur relatif terhadap akar disk yang dapat langsung disimpan ke kolom tabel.
Karena contoh ini tidak dijalankan melalui browser, unggahannya disimulasikan dengan helper UploadedFile::fake() yang membuat berkas uji berisi konten tetap. Pemanggilan storeAs() pada instance uji ini berjalan melalui jalur kode yang sama dengan controller.
# menyimpan unggahan uji seperti yang dilakukan controller
cd ~/aplikasi-catatan
php artisan tinker --execute='
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
Storage::disk("public")->delete("lampiran/lampiran-demo.txt");
$unggahan = UploadedFile::fake()->createWithContent("catatan.txt", "Isi lampiran catatan pertama.\n");
$jalur = $unggahan->storeAs("lampiran", "lampiran-demo.txt", "public");
echo "Tersimpan di: " . $jalur . PHP_EOL;
echo "URL: " . Storage::disk("public")->url($jalur) . PHP_EOL;
echo "Ukuran: " . Storage::disk("public")->size($jalur) . " byte" . PHP_EOL;
'Tersimpan di: lampiran/lampiran-demo.txt
URL: http://localhost:8000/storage/lampiran/lampiran-demo.txt
Ukuran: 30 byteNilai kembalian storeAs() adalah lampiran/lampiran-demo.txt, yaitu jalur relatif terhadap akar disk public, sedangkan metode url() menyusun alamat lengkapnya dari konfigurasi disk. Berkas uji kemudian dibaca melalui tautan simbolik untuk membuktikan bahwa jalur URL tadi benar-benar dilayani.
# membaca berkas uji melalui tautan public, lalu membersihkannya
cd ~/aplikasi-catatan
cat public/storage/lampiran/lampiran-demo.txt
wc -c public/storage/lampiran/lampiran-demo.txt
php artisan tinker --execute='
use Illuminate\Support\Facades\Storage;
Storage::disk("public")->delete("lampiran/lampiran-demo.txt");
echo "Berkas uji terhapus: " . var_export(Storage::disk("public")->missing("lampiran/lampiran-demo.txt"), true) . PHP_EOL;
'Isi lampiran catatan pertama.
30 public/storage/lampiran/lampiran-demo.txt
Berkas uji terhapus: truePembacaan melalui public/storage menghasilkan konten yang sama dengan yang diunggah, dan wc -c menghitung 30 byte sesuai ukurannya. Terakhir, berkas uji dihapus agar contoh dapat diulang dari keadaan yang bersih.
Disk S3 secara konseptual#
Driver s3 menempatkan berkas pada layanan penyimpanan awan yang kompatibel dengan S3, sehingga disk tidak lagi bergantung pada sistem berkas server. Kredensial dan nama bucket-nya dibaca dari variabel .env, dan driver ini menuntut paket tambahan league/flysystem-aws-s3-v3 yang belum terpasang pada proyek aplikasi catatan. Karena pemanggilannya memerlukan jaringan ke layanan luar, pembahasannya dibatasi pada konfigurasi berikut.
// tidak-dijalankan
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
],
// aksesnya identik dengan disk lain
Storage::disk('s3')->put('lampiran/lampiran-demo.txt', 'Isi lampiran catatan pertama.');Definisi tersebut sudah tersedia pada config/filesystems.php, sehingga perpindahan media cukup dilakukan dengan mengganti FILESYSTEM_DISK menjadi s3 atau dengan menyebut Storage::disk('s3') secara eksplisit. Tidak ada baris kode aplikasi yang berubah karena seluruh akses berjalan melalui API yang sama. Laravel mewakilkan driver local maupun s3 kepada library Flysystem, sehingga perbedaan media penyimpanannya diselesaikan pada lapisan library.
Kesalahan umum#
Kesalahan pertama adalah menyimpan unggahan dengan nama aslinya. Dua unggahan yang kebetulan bernama sama saling menimpa tanpa peringatan, dan nama yang dibentuk pengguna dapat memuat karakter yang tidak diinginkan. Gunakan metode store() yang menghasilkan nama hash, atau storeAs() dengan nama yang dibentuk aplikasi sendiri.
Kesalahan kedua adalah menempatkan berkas yang harus diakses browser pada disk local. Berkas di storage/app/private tidak dilayani web server, sehingga URL-nya selalu gagal dengan error 404 meskipun metode exists() mengembalikan true. Pindahkan berkasnya ke disk public, dan pastikan storage:link sudah dijalankan pada setiap server. Tautan public/storage tercantum dalam daftar gitignore bawaan, sehingga tautan itu tidak ikut terbawa saat proyek disalin ke server baru.
Kesalahan ketiga adalah menyusun jalur berkas secara manual dengan fungsi seperti public_path() atau penggabungan string. Kode semacam itu terikat pada satu media tertentu dan berhenti bekerja begitu disk dipindahkan ke S3. Seluruh akses berkas sebaiknya dilakukan melalui facade Storage, sehingga media penyimpanannya cukup diganti pada konfigurasi.
Ringkasan#
- Facade Storage mengakses berkas melalui disk yang didefinisikan pada
config/filesystems.php, dengan disk local menulis kestorage/app/privatedan disk public kestorage/app/public. - Perintah
storage:linkmembuat tautan simbolikpublic/storagesehingga berkas disk public dapat diminta melalui URL. - Unggahan tiba sebagai instance
UploadedFile, divalidasi terlebih dahulu, lalu disimpan denganstoreAs()ataustore()yang mengembalikan jalur relatif terhadap akar disk. - Driver
s3memindahkan media penyimpanan ke layanan awan melalui konfigurasi semata, tanpa mengubah kode aksesnya.
Referensi#
- Filesystem — Laravel — Dokumentasi resmi disk, facade Storage, operasi berkas, dan penanganan unggahan.
- Validation — Laravel
— Dokumentasi aturan validasi untuk berkas unggahan seperti
filedanmax.