Belajar Laravel 13 #9: File Storage (Upload Gambar)

Heru Iryanto
Heru Iryanto
Teacher, Content Creator and Full-Stack Developer

Di PHP Klasik, menyimpan foto profil yang diunggah (Upload) oleh pengunjung adalah hal yang sangat menegangkan. Kita harus memindahkan file tempnya menggunakan move_uploaded_file, lalu menaruhnya secara serampangan di folder htdocs/images.

Ini sangat fatal! Jika seorang peretas mengunggah file virus.php dengan berpura-pura sebagai gambar, dan Anda meletakkannya di folder yang terbuka untuk publik, peretas tersebut bisa mengambil alih server Anda!

Laravel memiliki brankas yang sangat terisolasi untuk menangani ini: Sistem Storage.

1️⃣ Dua Dimensi Penyimpanan (Private vs Public)

Periksa struktur folder Laravel Anda. Temukan folder bernama storage/. Semua file yang diunggah oleh pengunjung (Image, PDF, dll) WAJIB masuk ke dalam folder ini.

Namun ada aturannya:

  1. File yang diletakkan di storage/app/private/ sifatnya SANGAT RAHASIA (Private). Tidak akan bisa diakses dari browser luar. (Cocok untuk menyimpan Slip Gaji, Foto KTP, atau Laporan Keuangan PDF).
  2. File yang diletakkan di storage/app/public/ sifatnya BOLEH DILIHAT PUBLIK. (Cocok untuk menyimpan Foto Profil, Gambar Artikel/Berita).

Namun tunggu, meskipun namanya ‘public’, folder storage secara alami tidak bisa diakses dari browser luar (URL localhost:8000/storage/ akan Error 404). Lalu bagaimana cara menampilkan Foto Profil ke layar HTML pengunjung?

Untuk menampilkan foto yang aman tersebut ke publik, kita harus membengkokkan jaring laba-laba. Kita harus membuat lorong bawah tanah (Shortcut/Symlink) yang menghubungkan folder rahasia storage/app/public/ menuju pintu luar yang sah yaitu folder public/.

Buka Terminal Anda, dan teriakkan mantra mutlak ini SATU KALI SAJA di seumur hidup proyek Anda:

php artisan storage:link

Seketika, Anda akan melihat sebuah folder baru ajaib (dengan tanda panah Shortcut) tercipta di dalam folder public/storage. Kini, gambar-gambar Anda bebas diakses dengan aman lewat Browser!

3️⃣ Menambahkan inputan gambar ke form

Mari kita praktikkan cara menerima lemparan file foto dari Form HTML, lalu menyimpannya ke brankas rahasia.

Pastikan tag <form> di Blade HTML Anda sudah tertempel atribut wajib enctype="multipart/form-data".

Edit file create.blade.php di dalam folder resources/views/artikel/ untuk menambahkan enctype="multipart/form-data" dan tag input type="file" untuk gambar:

<!-- ... kode sebelumnya ... -->
<form action="{{ route('artikel.store') }}" method="POST" class="space-y-4" enctype="multipart/form-data">
    @csrf
    <div>
        <label for="judul" class="block text-sm font-medium text-gray-700">Judul</label>
        <input type="text" name="judul" id="judul" class="w-full border p-2 rounded-md mt-2">
    </div>

    <div>
        <label for="konten" class="block text-sm font-medium text-gray-700">Konten</label>
        <textarea name="konten" id="konten" rows="5" class="w-full border p-2 rounded-md mt-2"></textarea>
    </div>

    <div>
        <label for="gambar" class="block text-sm font-medium text-gray-700">Gambar</label>
        <input type="file" name="gambar" id="gambar" class="w-full border p-2 rounded-md mt-2">
    </div>

    <button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition">Simpan Artikel</button>
</form>
<!-- ... kode sebelumnya ... -->

4️⃣ Menambahkan kolom gambar di database

Di database kita saat ini belum ada kolom untuk menyimpan gambar, maka kita perlu menambahkan kolom gambar pada tabel artikel. Berikut cara menambahkan migration field gambar pada tabel artikel, buka terminal dan jalankan perintah berikut ini:

php artisan make:migration add_gambar_to_artikels_table --table=artikels

Di folder migrations akan terbentuk file dengan nama YYYYMMDDHHMMSS_add_gambar_to_artikels_table.php (sesuai tanggal dan waktu pembuatan). Edit file tersebut seperti berikut ini:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('artikels', function (Blueprint $table) {
            // Tambahkan field gambar setelah field konten
            $table->string('gambar')->nullable()->after('konten');
        });
    }

    public function down(): void
    {
        Schema::table('artikels', function (Blueprint $table) {
            // Hapus field gambar jika migration di-rollback
            $table->dropColumn('gambar');
        });
    }
};

Jalankan migration untuk menambahkan kolom gambar ke tabel artikel:

php artisan migrate

Cek di phpMyAdmin/TablePlus, kolom gambar pada tabel artikels harusnya sudah ada.

5️⃣ Menambahkan Kode Eksekusi Upload di Controller

Buka file Controller Anda yang berfungsi menangani lemparan Form tersebut:

// Aksi 3: Menyimpan Data Baru ke Database
public function store(Request $request) {
    // Validasi Data
    $request->validate([
        'judul' => 'required',
        'konten' => 'required',
        'gambar' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048', // Validasi gambar opsional
    ]);

    // Simpan data ke database
    //Artikel::create($request->all());
    $baru = new Artikel();
    $baru->judul = $request->judul;         //Ambil dari Kotak "judul"
    $baru->konten = $request->konten;   //Ambil dari Kotak "konten"
    
    // Simpan Gambar ke Storage
    if ($request->hasFile('gambar')) {
        $baru->gambar = $request->file('gambar')->store('gambars', 'public');
    }
    
    $baru->save(); //DORONG ke Database!

    // Redirect ke halaman daftar artikel
    return redirect()->route('artikel.index')->with('success', 'Artikel berhasil ditambahkan!');
}

Informasi Tambahan: Berikut adalah cara lain untuk menyimpan gambar ke database dengan menggunakan method move(). Biasanya cara ini tanpa storage link (yaitu dengan memindahkan file gambar secara langsung ke folder public/images). Cara ini kurang aman dan rawan bentrok (tabrakan) nama file jika ada user lain yang mengupload file dengan nama yang sama, sehingga tidak direkomendasikan untuk digunakan pada project besar (jadi kita tidak usah menggunakan cara ini ya).

// Upload gambar
if ($request->hasFile('gambar')) {
    $imageName = time().'.'.$request->gambar->extension();
    $request->gambar->move(public_path('images'), $imageName);
}

// Simpan data ke database
Portofolio::create([
    'judul' => $request->judul,
    'deskripsi' => $request->deskripsi,
    'gambar' => $imageName,
]);

Pada kodingan store diatas, kita melakukan validasi terhadap inputan gambar dengan menggunakan $request->validate(), yang artinya:

  • nullable: Gambar bersifat opsional (boleh tidak diisi).
  • image: Data yang diupload harus berformat gambar (jpg, png, gif, dll).
  • mimes:jpeg,png,jpg,gif: Format file yang diizinkan.
  • max:2048: Ukuran file maksimum 2MB (2048KB).

Lalu pada kodingan if ($request->hasFile('gambar')) artinya:

  • hasFile(): Method yang digunakan untuk mengecek apakah ada file yang diupload.
  • file(): Method yang digunakan untuk mendapatkan file yang diupload.
  • store(): Method yang digunakan untuk menyimpan file yang diupload. Fungsi ->store() akan secara ajaib men-generate nama acak antikembar (Misal: sdksd78syd8sydhsdu.jpg) agar tidak saling menimpa!. File gambar akan disimpan folder storage/app/public/gambars/. Nama folder gambars tersebut adalah custom yang bisa kamu ganti-ganti.

Sekarang coba tambahkan artikel baru dengan gambar, harusnya sekarang gambar sudah bisa masuk ke database dan folder storage/app/public/gambars/. Namun gambar belum bisa tampil di halaman index maupun detail! Di materi dibawah ini, kita akan segera memunculkannya.

6️⃣ Menampilkan Gambar ke Piring Blade

Alamat gambar kini sudah tercatat di MySQL/TablePlus (Contoh: gambars/sdksd78syd8sydhsdu.jpg), dan fisik filenya ada di brankas dalam. Bagaimana cara memanggilnya di HTML (<img src="...">)?

Kita gunakan jembatan storage:link yang sudah kita bangun tadi dipadukan dengan sihir penunjuk arah (Helper Asset) bawaan Blade.

Buka file resources/views/index.blade.php dan letakkan kode pemanggil gambar dibawah ini. Letakkan diatas judul <h3>:

@if ($artikel->gambar)
    <img src="{{ asset('storage/' . $artikel->gambar) }}" alt="Gambar Artikel" class="w-full h-64 object-cover mb-4 rounded-md">
@endif                

Arti kode diatas adalah:

  • @if ($artikel->gambar): Ini adalah sintaks Blade (mirip PHP) untuk melakukan pengecekan bersyarat. Artinya, “Jika variabel $artikel memiliki nilai pada properti gambar (artinya user upload gambar), maka tampilkan kodenya”. Jika null atau kosong, maka kode ini akan dilewati.

  • <img ...>: Ini adalah tag HTML standar untuk menampilkan gambar.

  • src="{{ asset('storage/' . $artikel->gambar) }}": Ini adalah bagian terpentingnya.

    • asset(): Adalah Helper bawaan Laravel yang berfungsi untuk menghasilkan URL absolut dari folder public. Karena kita sudah membuat link tadi, public/storage sekarang merujuk ke storage/app/public.
    • 'storage/' . $artikel->gambar: Kita menyambung string (menggabungkan teks) antara folder storage/ (yang menjadi pintu publik) dengan nama file yang tersimpan di database (misal: gambars/kucing.jpg).
    • Hasilnya akan menjadi: http://localhost:8000/storage/gambars/kucing.jpg.
  • alt="Gambar Artikel": Atribut alternatif jika gambar gagal dimuat.

  • class="...": Atribut CSS untuk mengatur ukuran dan tampilan gambar agar rapi.

Lakukan hal yang sama untuk halaman detail.blade.php di dalam resources/views/artikel/ agar gambarnya bisa muncul juga. Letakkan diatas judul <h1>.

7️⃣ Halaman Edit Artikel

Sama halnya untuk halaman create, kita juga harus merubah halaman edit agar gambar bisa muncul dihalaman edit atau bisa mengganti gambar yang sudah ada.

Silakan buka file resources/views/artikel/edit.blade.php. Yang pertama harus kita ubah adalah bagian form. Kita harus menambahkan enctype="multipart/form-data" pada form tersebut. Sehingga form nya menjadi seperti ini:

<form action="{{ route('artikel.update', $artikel->id) }}" method="POST" enctype="multipart/form-data">

Yang kedua, kita mau menambahkan gambar saat ini yang sudah ada dan inputan untuk gambar baru. Sebelum tombol “Update Artikel”, silakan tambahkan kode berikut ini:

@if ($artikel->gambar)
    <div class="mb-4">
        <label class="block text-gray-700">Gambar Saat Ini</label>
        <img src="{{ asset('storage/' . $artikel->gambar) }}" alt="Gambar Artikel" class="mt-2 w-64 h-auto">
    </div>
@endif

<div class="mb-4">
    <label class="block text-gray-700">Ganti Gambar</label>
    <input type="file" name="gambar" class="w-full border p-2 rounded-md mt-2">
</div>

Yang terakhir, kita harus merubah kodingan update di controller menjadi seperti ini:

// Aksi 6: Menyimpan Perubahan Data
public function update(Request $request, $id) {
    // Validasi Data
    $request->validate([
        'judul' => 'required',
        'konten' => 'required',
        'gambar' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048', // Validasi gambar opsional
    ]);

    // Cari data berdasarkan ID
    $artikel = Artikel::find($id);
    $artikel->judul = $request->judul;
    $artikel->konten = $request->konten;

    // Jika ada gambar baru
    if ($request->hasFile('gambar')) {
        // Hapus gambar lama
        if ($artikel->gambar) {
            Storage::disk('public')->delete($artikel->gambar);
        }

        // Simpan gambar baru
        $artikel->gambar = $request->file('gambar')
            ->store('gambars', 'public');
    }

    $artikel->save();

    return redirect('/artikel')->with('success', 'Artikel berhasil diupdate!');
}

Sekarang coba edit artikel yang sudah ada dan ganti gambarnya. Harusnya sekarang gambar yang lama akan terhapus dan digantikan dengan gambar yang baru. Begitupun dengan file gambar yang berada di dalam folder storage/app/public/gambars/ juga akan terhapus. Gampang bukan?.

🎓 8. Latihan: “Sentuhan Akhir - Website Artikel”

Materi dasar laravel kita sudah selesai. Sebagai tugas kalian, coba kalian percantik “Website Artikel Onlline” Anda, sehingga menjadi semenarik mungkin agar orang ingin terus berkunjung ke website Anda. Anda juga bisa menambahkan fitur-fitur menarik yang belum dibahas disini. Fitur yang wajib ada adalah:

  • Tampilan yang lebih modern
  • Responsive design
  • Halaman utama welcome.blade.php yang menampilkan grid card artikel (6 artikel terbaru)
  • Pagination
  • Halaman detail detail.blade.php yang menampilkan detail artikel yang menarik.

Ini adalah contoh/kerangka kode untuk halaman utama welcome.blade.php. Anda bisa mengembangkan/menghiasnya sesuai kreasi Anda.

<!DOCTYPE html>
<html>
<head>
    <title>Artikel</title>
</head>
<body>

    <h1>Daftar Artikel</h1>

    @if ($artikels->count())

        @foreach ($artikels as $artikel)

            <div>
                @if ($artikel->gambar)
                    <img src="{{ asset('storage/' . $artikel->gambar) }}"
                         width="200">
                @endif

                <h2>{{ $artikel->judul }}</h2>

                <p>
                    {{ Str::limit(strip_tags($artikel->konten), 100) }}
                </p>

                <a href="{{ route('artikel.show', $artikel->id) }}">
                    Baca Selengkapnya
                </a>
            </div>

            <hr>

        @endforeach

        {{ $artikels->links() }}

    @else

        <p>Belum ada artikel.</p>

    @endif

</body>
</html>

Penjelasan:

  • @if ($artikels->count()): Ini adalah sintaks Blade untuk melakukan pengecekan bersyarat. Artinya, “Jika variabel $artikels memiliki nilai pada properti count (artinya ada data artikel), maka tampilkan kodenya”. Jika null atau kosong, maka kode ini akan dilewati.

  • @else: Sintaks Blade untuk menampilkan kode jika kondisi @if tidak terpenuhi. Artinya, “Jika variabel $artikels tidak memiliki nilai pada properti count (artinya tidak ada data artikel), maka tampilkan kode di dalam @else dan @endif”.

  • @foreach: Sintaks Blade untuk melakukan iterasi (perulangan) pada variabel $artikels. Artinya, “Untuk setiap data dalam variabel $artikels, tampilkan kodenya”. Jika null atau kosong, maka kode ini akan dilewati. Jangan lupa untuk menutup perulangan dengan @endforeach.

  • $artikels->links(): Sintaks Blade untuk menampilkan tautan paginasi. Artinya, “Tampilkan tautan halaman”.

Untuk contoh route nya bisa kita buat seperti ini agar tidak menambah function lagi di ArtikelController (Anda bisa menambahkan/mengubahnya sesuai kebutuhan) :

use App\Models\Artikel;

Route::get('/', function () {
    // Artikel bisa dibaca siapa saja tanpa login, ditampilkan dengan pagination
    $artikels = Artikel::latest()->paginate(6);

    return view('welcome', compact('artikels'));
});

Selamat mencoba dan jangan menyerah!

🗽 Kesimpulan

Ini adalah teknik tingkat lanjut yang wajib Anda pahami. Jika Anda membuat Error saat mengatur fungsi store() dan storage:link, seringkali gambar akan rusak (Memunculkan ikon kertas sobek di HTML). Anda butuh ketelitian jalur direktori yang sangat tinggi.

Sejauh ini, Anda telah dijejali dengan puluhan senjata mematikan (Rute, Controller, Eloquent, Migration, Middleware, dan Storage). Teori saja tidak akan pernah membekas di otak tanpa praktikum lapangan.

Mari terjun ke medan pertempuran kelulusan di Proyek Masterpiece: Aplikasi CMS “Portfolio” Modern.