Penyimpanan berkas

Facade Storage dengan disk local dan S3, beserta upload dari formulir.

Daftar isi

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.

DiskDriverLokasi berkasPeruntukan
locallocalstorage/app/privateberkas privat aplikasi
publiclocalstorage/app/publicberkas yang disajikan melalui URL
s3s3bucket pada layanan penyimpanan S3berkas 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.

bash
# 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;
'
text Keluaran
Ada: true
Isi catatan pertama disimpan lewat disk local.
Ukuran: 47 byte
Jalur: /home/kal/aplikasi-catatan/storage/app/private/arsip-demo.txt

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

bash
# 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;
'
text Keluaran
Isi catatan pertama disimpan lewat disk local.
Setelah dihapus: true

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

bash
# membuat tautan public/storage menuju storage/app/public
cd ~/aplikasi-catatan
rm -f public/storage
php artisan storage:link
readlink public/storage
text Keluaran
   INFO  The [public/storage] link has been connected to [storage/app/public].

/home/kal/aplikasi-catatan/storage/app/public

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

blade form-lampiran.blade.php
<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.

php LampiranController.php
// 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.

bash
# 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;
'
text Keluaran
Tersimpan di: lampiran/lampiran-demo.txt
URL: http://localhost:8000/storage/lampiran/lampiran-demo.txt
Ukuran: 30 byte

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

bash
# 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;
'
text Keluaran
Isi lampiran catatan pertama.
30 public/storage/lampiran/lampiran-demo.txt
Berkas uji terhapus: true

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

php Contoh: definisi disk s3 dan pemakaiannya
// 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 ke storage/app/private dan disk public ke storage/app/public.
  • Perintah storage:link membuat tautan simbolik public/storage sehingga berkas disk public dapat diminta melalui URL.
  • Unggahan tiba sebagai instance UploadedFile, divalidasi terlebih dahulu, lalu disimpan dengan storeAs() atau store() yang mengembalikan jalur relatif terhadap akar disk.
  • Driver s3 memindahkan media penyimpanan ke layanan awan melalui konfigurasi semata, tanpa mengubah kode aksesnya.

Referensi#