Laravel 8 - #21 - Upload Image
Pada pembahasan kali ini kita akan mencoba menambahkan sebuah feature untuk melengkapi CRUD dari blog kita, yaitu menambahkan feature upload gambar yang dimana nantinya user dapat mengupload gambar sendiri dari komputer nya untuk menggantikan gambar yang sebelumnya kita ambil dari API nya Unsplash.

Arman Dwi Pangestu
6 Januari 2024•1 menit baca

Pendahuluan
Pada pembahasan kali ini kita akan mencoba menambahkan sebuah feature untuk melengkapi CRUD dari blog kita, yaitu menambahkan feature upload gambar yang dimana nantinya user dapat mengupload gambar sendiri dari komputer nya untuk menggantikan gambar yang sebelumnya kita ambil dari API nya Unsplash.
Namun, jika kita lihat tabel posts didalam database kita saat ini belum terdapat field untuk menyimpan gambar, nantinya akan kita perbaiki. Nah, namun jika postingan nya tidak memiliki gambar maka gambar yang akan digunakan tetap menggunakan API unsplash.
New Post Input Gambar
Untuk melakukannya, pertama-tama kita tambahkan field input gambar di file view create.blade.php kita
Catatan:
Perlu diingat, ketika kita akan bekerja dengan file didalam form, maka kita tambahkan attribute
enctypedidalam element form nya yang isi dari value nya adalahmultipart/form-data. Karena jika tidak menggunakan attribute tersebut maka form tersebut tidak bisa menangani file, namun jika menggunakan attribute tersebut maka form tersebut bisa menangani dua hal:
- Semua input-an dalam bentuk text akan diambil menggunakan request biasa
- Jika terdapat input-an file maka akan diambil menggunakan request file
multipart/form-dataJika tidak ada attribute tersebut maka file kalian tidak akan bisa di upload
<form action="/dashboard/posts" method="POST" class="mb-5" enctype="multipart/form-data">
@csrf
...
<div class="mb-3">
<label for="image" class="form-label">Post Image</label>
<input class="form-control" type="file" id="image" name="image">
</div>
...
<button type="submit" class="btn btn-primary">Create Post</button>
</form>
Bagaimana Laravel Menangani Upload File?
Nah sebelum kita jalankan upload gambar nya, kita akan lihat terlebih dahulu bagaimana Laravel ini menangani upload sebuah file tersebut seperti apa. Kita bisa buka terlebih dahulu file controller DashboardPostController.php
Catatan: Tips
Method
ddddisini artinya adalah:
- Dump
- Die
- Debug
public function store(Request $request)
{
ddd($request);
...
Post::create($validateData);
return redirect('/dashboard/posts')->with('success', 'New post has been added!');
}
Sekarang jika kalian mencoba mengisikan form input di new post dengan data sembarang dan upload sebuah gambar maka akan muncul tampilan dari method ddd nya


Dapat kalian lihat terdapat banyak sekali informasi, namun yang kita butuhkan hanya yang didalam request

Jika kalian lihat, maka akan bertanya-tanya, dimana image yang sudah kita upload? tenang, image yang kalian upload masuk kedalam files, sehingga itulah mengapa kita membutuhkan multipart. Jadi yang string masuk nya kedalam request dan yang file masuknya kedalam files

Didalam files tersebut terdapat beberapa informasi seperti original name, mimeType atau bentuk file atau extension, kemudian terdapat lokasi file penyimpanan dan nama sementara, ukurang file nya dan seterusnya.
Bagaimana Cara Menyimpan File?
Nah sekarang pertanyaan nya, bagaimana cara menyimpan file yang sudah di upload tersebut? caranya cukup gampang, misalkan disini kita akan return value pada method store nya sehingga kode dibawah nya tidak akan dijalankan
public function store(Request $request)
{
return $request->file('image')->store('post-images');
...
}
Sekarang jika kita mencoba kembali melakukan upload gambar, maka akan muncul tulisan dari return value store tersebut yaitu dengan format nama-folder/nama-file-hash seperti gambar dibawah ini

File tersebut sebetulnya sudah ter-upload, dimana letak file tersebut sekarang? kalian bisa lihat di folder /storage/app/post-images

Keren bukan? hanya satu baris kode doang kita bisa upload file. Namun, hal tersebut banyak yang harus kita perbaiki seperti pengaturan lokasi file penyimpanan atau PATH nya. Jika kalian bertanya-tanya "mengapa sih disimpen nya di folder /storage? bukan ditempat yang kita mau", untuk mencari jawaban tersebut kita bisa lihat di dokumentasi resmi Laravel nya mengenai File System atau File Storage.
Jika dikutip dari web resmi Laravel nya, Laravel sudah menyediakan sebuah file system yang powerfull berkat library yang namanya Flysystem. Sehingga jika nanti kedepannya kalian ingin meng-integrasikan aplikasi kalian agar bisa meng-upload ke beberapa tempat seperti local filesystem, SFTP atau Amazon S3.
Custom PATH
Sekarang bagaimana cara mengatur nya? kalian bisa pergi ke file config/filesystems.php
'default' => env('FILESYSTEM_DRIVER', 'local'),
Secara default itu adalah local namun sebelum local tersebut laravel akan mengecek variabel di .env dengan nama FILESYSTEM_DRIVER. Nah, local tersebut berada di storage_path('app)
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app'),
],
...
]
Sehingga file yang di upload tadi tersimpan di /storage/app. Kedepannya kita tidak ingin menyimpan nya di lokasi tersebut, karena kita ingin agar file-file yang di upload tersebut dapat diaskses secara public untuk ditampilkan di halaman blog kita jadi kita harus memindahkannya ke lokasi /storage/app/public.
Agar tersimpan nya ke public maka default filesystem nya jangan di local namun kita pindahkan ke public. Caranya kalian bisa ganti menjadi seperti ini
'default' => env('FILESYSTEM_DRIVER', 'public'),
Atau kalian bisa tambahkan variabel baru dengan nama FILESYSTEM_DRIVER di .env kalian dengan value nya adalah public
FILESYSTEM_DRIVER=public
Maka sekarang jika kita mencoba upload kembali file nya, akan tersimpan di lokasi /storage/app/public/post-images

Nah namun hal tersebut masih terdapat problem jika kita akses langsung melalui browser dengan cara copy relative path dari file tersebut

Permission
Problem diatas tersebut nantinya akan menyebabkan kita tidak bisa menggunakan atau menampilkan gambar tersebut walaupun sudah disimpan di folder public. Hal tersebut terjadi karena folder /storage/app/public itu harus kita hubungkan terlebih dahulu dengan folder /public yang ada didalam aplikasi kita.
Folder /public tersebutlah yang benar-benar bisa diakses oleh user, contohnya disini saya mempunyai gambar saya sendiri


Symbolic Link
Sekarang, bagaimana cara menghubungkan folder /storage/app/public kedalam folder /public? Caranya kita cukup buatkan symlink atau symbolic link dengan perintah artisan berikut ini
php artisan storage:link

Maka sekarang akan muncul folder storage didalam folder /public
Catatan: Tips
Bisa kalian lihat, terdapat tanda panah di ujung kanan folder nya, itu menandakan bahwa folder tersebut adalah symbolic link

Sehingga nantinya jika kita ingin meng-akses file nya, kita bisa gunakan method yang namanya asset
echo asset('storage/file.txt');
Jika sekarang kita coba akses melalui browser relative path nya, maka sekarang tidak akan lagi muncul error 404 | NOT FOUND

Hal tersebut yang perlu perbaiki sehingga sekarang kita sudah siap meng-upload file nya.
Skema Migration Baru
Namun sebelum kita jalankan perlu di ingat bahwa kita belum mempunyai field untuk menyimpan gambar didalam tabel posts nya, oleh karena itu kita tambahkan field baru didalam file migration nya
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('category_id');
$table->foreignId('user_id');
$table->string('title');
$table->string('slug')->unique();
$table->string('image')->nullable();
$table->text('excerpt');
$table->text('body');
$table->timestamp('publish_at')->nullable();
$table->timestamps();
});
}
Sebelum kita jalankan migration nya, kita buka terlebih dahulu file seeder nya
public function run()
{
User::create([
'name' => 'Arman Dwi Pangestu',
'username' => 'devnull',
'email' => 'arman@gmail.com',
'password' => bcrypt('password')
]);
User::factory(3)->create();
Category::create([
'name' => 'Web Programming',
'slug' => 'web-programming'
]);
Category::create([
'name' => 'Web Design',
'slug' => 'web-design'
]);
Category::create([
'name' => 'Personal',
'slug' => 'personal'
]);
Post::factory(20)->create();
}
Sekarang kita jalankan perintah migration nya dengan artisan
php artisan migrate:fresh --seed
Maka sekarang seharusnya akan muncul field baru dengan nama image pada tabel posts
mysql> DESCRIBE posts;
+-------------+-----------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+-----------------+------+-----+---------+----------------+
| id | bigint unsigned | NO | PRI | NULL | auto_increment |
| category_id | bigint unsigned | NO | | NULL | |
| user_id | bigint unsigned | NO | | NULL | |
| title | varchar(255) | NO | | NULL | |
| slug | varchar(255) | NO | UNI | NULL | |
| image | varchar(255) | YES | | NULL | |
| excerpt | text | NO | | NULL | |
| body | text | NO | | NULL | |
| publish_at | timestamp | YES | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+-------------+-----------------+------+-----+---------+----------------+
11 rows in set (0.03 sec)
Validasi Upload Gambar
Setelah field image didalam tabel posts nya kita buat, selanjutnya kita balik lagi ke validasi penyimpanan data di DashboardPostController.php pada method store nya
Catatan: Tips
Pada validasi
image|filekita bisa masukkan beberapa kriteria ukuran file nya dalam ukuran kilobyte, misalkan
image|file|min:1024: artinya adalah file yang di uploadminimumsize nya adalah1MB
image|file|size:1024: artinya adalah file yang di uploadharus sama persissize nya adalah1MB
image|file|max:1024: artinya adalah file yang di uploadmaksimalsize nya adalah1MBDan jika kita tidak tambahkan validasi
filedi depannya, maka akan dianggap validasi karakter atau integer bukan file
public function store(Request $request)
{
$validateData = $request->validate([
'title' => 'required|max:255',
'slug' => 'required|unique:posts',
'category_id' => 'required',
'image' => 'image|file|max:1024',
'body' => 'required'
]);
$validateData['user_id'] = auth()->user()->id;
$validateData['excerpt'] = Str::limit(strip_tags($request->body), 200);
Post::create($validateData);
return redirect('/dashboard/posts')->with('success', 'New post has been added!');
}
Selanjutnya kita balik lagi ke view create.blade.php untuk memberikan error nya jika validasi nya tidak lolos
Catatan:
Problem dari upload image disini kita tidak bisa menggunakan method
olduntuk menangkap value dari gambar sebelumnya karena hal tersebut terjadi karena pertimbangan security untuk mencegah agar orang lain tidak mengetahui sturktur directory kita.
<form action="/dashboard/posts" method="POST" class="mb-5" enctype="multipart/form-data">
@csrf
...
<div class="mb-3">
<label for="image" class="form-label">Post Image</label>
<input class="form-control @error('image')
is-invalid
@enderror" type="file" id="image" name="image">
@error('image')
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
</div>
...
</form>
Jika sekarang kita mencoba meng-upload file diatas 1MB maka validasi error nya akan muncul karena kita set di validasi nya maksimal size file yang bisa di upload adalah 1MB

Dan jika kita coba upload file selain gambar maka akan muncul juga validasi error nya, misalkan disini saya mencoba upload file pdf

Selanjutnya kita lakukan pengecekan jika user nya tidak mengisikan atau meng-upload gambar nya, karena yang kita inginkan jika tidak ada gambar maka gunakan gambar dari unsplash
public function store(Request $request)
{
$validateData = $request->validate([
'title' => 'required|max:255',
'slug' => 'required|unique:posts',
'category_id' => 'required',
'image' => 'image|file|max:2048',
'body' => 'required'
]);
if ($request->file('image')) {
$validateData['image'] = $request->file('image')->store('post-images');
}
$validateData['user_id'] = auth()->user()->id;
$validateData['excerpt'] = Str::limit(strip_tags($request->body), 200);
Post::create($validateData);
return redirect('/dashboard/posts')->with('success', 'New post has been added!');
}
Sekarang kita coba buat postingan baru dengan ukuran gambar yang sesuai



View Menggunakan Gambar Dari Database
Sekarang sisanya kita tinggal ubah kode dibagian view nya agar menggunakan gambar dari database jika field nya memiliki gambar dan jika tidak memiliki gambar maka gunakan dari unsplash, kita mulai dari view show.blade.php
@extends('dashboard.layouts.main')
@section('container')
<div class="container">
<div class="row mb-5">
<div class="col-lg-8">
<h1 class="my-3">{{ $post->title }}</h1>
...
@if ($post->image)
<div style="max-height: 350px; overflow: hidden">
<img src="{{ asset('storage/' . $post->image) }}" alt="{{ $post->category->name }}" class="img-fluid mt-3">
</div>
@else
<img src="https://source.unsplash.com/1200x400?{{ $post->category->name }}" alt="{{ $post->category->name }}" class="img-fluid mt-3">
@endif
...
</div>
</div>
</div>
@endsection
Maka sekarang akan tampil gambar yang sudah kita upload sebelumnya

Sisanya kita cukup perbaiki src pada view yang menangani postingan blog di bagian depan (bukan dashboard) di file posts.blade.php
@extends('layouts.main')
@section('container')
<h1 class="mb-3 text-center">{{ $title }}</h1>
...
@if ($posts->count())
<div class="card mb-3">
@if ($posts[0]->image)
<div style="max-height: 400px; overflow: hidden">
<img src="{{ asset('storage/' . $posts[0]->image) }}" alt="{{ $posts[0]->category->name }}" class="img-fluid">
</div>
@else
<img src="https://source.unsplash.com/1200x400?{{ $posts[0]->category->name }}" class="card-img-top" alt="{{ $posts[0]->category->name }}">
@endif
...
</div>
<div class="container">
<div class="row mb-5">
@foreach ($posts->skip(1) as $post)
<div class="col-md-4 mb-3">
<div class="card">
...
@if ($post->image)
<img src="{{ asset('storage/' . $post->image) }}" alt="{{ $post->category->name }}" class="img-fluid">
@else
<img src="https://source.unsplash.com/500x400?{{ $post->category->name }}" class="card-img-top" alt="{{ $post->category->name }}">
@endif
...
</div>
</div>
@endforeach
</div>
</div>
@else
<p class="text-center fs-4">No post found.</p>
@endif
<div class="d-flex justify-content-end">
{{ $posts->links() }}
</div>
@endsection
Sekarang seharusnya sudah tampil gambar dari database pada postingan frontend nya

Terakhir paling pada halaman view single post nya di file post.blade.php
@if ($post->image)
<div style="max-height: 350px; overflow: hidden">
<img src="{{ asset('storage/' . $post->image) }}" alt="{{ $post->category->name }}" class="img-fluid">
</div>
@else
<img src="https://source.unsplash.com/1200x400?{{ $post->category->name }}" alt="{{ $post->category->name }}" class="img-fluid">
@endif
