Pendahuluan
Selain fitur yang memungkinkan user untuk menambahkan data produk satu-persatu, maka alangkah lebih menariknya lagi jika kita menyediakan sebuah fitur yang memungkinkan user untuk menambahkan produk secara massal. Serial kali ini akan membahas bagaimana cara menambahkan data tersebut secara massal dengan menggunakan data yang telah disusun pada sebuah file yang berformat Excel. Tidak hanya itu saja, gambar masing-masing produk juga akan secara otomatis di-set sesuai url yang telah dimasukkan pada templating mass upload yang tersedia.
Adapun bagian yang belum dikerjakan pada artikel sebelumnya, yakni, proses edit data produk juga akan dibahas didalam artikel ini. Selain berinteraksi dengan fitur yang telah disediakan oleh Laravel, maka kita juga akan belajar bagaimana berinteraksi dengan eksternal package.
Baca Juga: Aplikasi E-Commerce Laravel 6 #5: Upload Products
External Package & Form Upload
Ketika menggunakan file dengan format tertentu, misalnya office, maka salah satu langkah yang baiknya ditempuh adalah dengan menggunakan external package sehingga kita tidak perlu lagi membuatnya dari awal hanya untuk persoalan yang sudah diselesaikan oleh orang lain.
Pada command line, install Laravel Excel dengan command
composer require maatwebsite/excel
Package ini telah menyediakan fungsi import maupun export file Excel, sehingga sebagai programmer kita hanya menggunakannya saja untuk menyelesaikan case yang sedang dihadapi. Langkah selanjutnya adalah dengan membuat form dimana user bisa meng-upload dokumen terkait yang berisi data produk. Buka file ProductController.php
dan tambahkan method
public function massUploadForm()
{
$category = Category::orderBy('name', 'DESC')->get();
return view('products.bulk', compact('category'));
}
Kemudian buat file bulk.blade.php
dan letakkan didalam folder resources/views/products
, kemudian tambahkan code berikut
@extends('layouts.admin')
@section('title')
<title>Mass Upload</title>
@endsection
@section('content')
<main class="main">
<ol class="breadcrumb">
<li class="breadcrumb-item">Home</li>
<li class="breadcrumb-item active">Product</li>
</ol>
<div class="container-fluid">
<div class="animated fadeIn">
<!-- ARAH ACTIONYA ADALAH KE ROUTING DENGAN NAME product.saveBulk -->
<form action="{{ route('product.saveBulk') }}" method="post" enctype="multipart/form-data" >
@csrf
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-body">
@if (session('success'))
<div class="alert alert-success">{{ session('success') }}</div>
@endif
<!-- SETIAP USER HARUS MEMILIH KATEGORI PRODUK TERKAIT -->
<div class="form-group">
<label for="category_id">Kategori</label>
<select name="category_id" class="form-control">
<option value="">Pilih</option>
@foreach ($category as $row)
<option value="{{ $row->id }}" {{ old('category_id') == $row->id ? 'selected':'' }}>{{ $row->name }}</option>
@endforeach
</select>
<p class="text-danger">{{ $errors->first('category_id') }}</p>
</div>
<div class="form-group">
<label for="file">File Excel</label>
<input type="file" name="file" class="form-control" value="{{ old('file') }}" required>
<p class="text-danger">{{ $errors->first('file') }}</p>
</div>
<div class="form-group">
<button class="btn btn-primary btn-sm">Upload</button>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</main>
@endsection
Selanjutnya adalah mendefinisikan routing-nya, buka file routes/web.php
dan tambahkan route
Route::resource('product', 'ProductController')->except(['show']); //BAGIAN INI KITA TAMBAHKAN EXCETP KARENA METHOD SHOW TIDAK DIGUNAKAN
Route::get('/product/bulk', 'ProductController@massUploadForm')->name('product.bulk'); //TAMBAHKAN ROUTE INI
Bagian terakhir dari sub-bab ini adalah dengan menambahkan tombol dari halaman list produk yang berfungsi untuk mengarahkan kehalaman mass upload tersebut. Buka file /views/products/index.blade.php
dan modifikasi bagian berikut
<!-- CODE SEBELUMNYA -->
<div class="card-header">
<h4 class="card-title">
List Product
<div class="float-right">
<a href="{{ route('product.bulk') }}" class="btn btn-danger btn-sm">Mass Upload</a>
<a href="{{ route('product.create') }}" class="btn btn-primary btn-sm">Tambah</a>
</div>
</h4>
</div>
<!-- CODE SETELAHNYA -->
Note: Cukup modifikasi bagian header dari card yakni dengan menambahkan tombol Mass Upload.
Import Data & Dispatching Job
Bagian ini akan dikelompokkan menjadi dua bagian, pertama adalah membuat jadwal untuk setiap request agar proses-nya tidak dibebankan kepada user sehingga proses time pada browser user menjadi cepat. Proses tersebut akan kita pindahkan ke-background dimana bebannya akan tanggung oleh server. Kedua adalah meng-extract data kemudian diolah dan diteruskan untuk disimpan ke-database.
Jika mengikuti alur diatas maka pertama kita definisikan routing-nya terlebih dahulu, buka file routes/web.php
dan tambahkan code berikut
Route::post('/product/bulk', 'ProductController@massUpload')->name('product.saveBulk');
Kemudian buka file ProductController.php
dan tambahkan method massUpload()
public function massUpload(Request $request)
{
//VALIDASI DATA YANG DIKIRIM
$this->validate($request, [
'category_id' => 'required|exists:categories,id',
'file' => 'required|mimes:xlsx' //PASTIKAN FORMAT FILE YANG DITERIMA ADALAH XLSX
]);
//JIKA FILE-NYA ADA
if ($request->hasFile('file')) {
$file = $request->file('file');
$filename = time() . '-product.' . $file->getClientOriginalExtension();
$file->storeAs('public/uploads', $filename); //MAKA SIMPAN FILE TERSEBUT DI STORAGE/APP/PUBLIC/UPLOADS
//BUAT JADWAL UNTUK PROSES FILE TERSEBUT DENGAN MENGGUNAKAN JOB
//ADAPUN PADA DISPATCH KITA MENGIRIMKAN DUA PARAMETER SEBAGAI INFORMASI
//YAKNI KATEGORI ID DAN NAMA FILENYA YANG SUDAH DISIMPAN
ProductJob::dispatch($request->category_id, $filename);
return redirect()->back()->with(['success' => 'Upload Produk Dijadwalkan']);
}
}
Jangan lupa pada file yang sama tambahkan use statement
use App\Jobs\ProductJob;
Class ProductJob
belum ada, maka tugas selanjutnya adalah membuat file tersebut dengan command
php artisan make:job ProductJob
Kemudian buat file ProductJob.php
yang berada didalam folder app/Jobs
dan modifikasi menjadi
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Imports\ProductImport; //IMPORT CLASS PRODUCTIMPORT YANG AKAN MENG-HANDLE FILE EXCEL
use Illuminate\Support\Str;
use App\Product;
use File;
class ProductJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $category;
protected $filename;
//KARENA DISPATCH MENGIRIMKAN 2 PARAMETER
//MAKA KITA TERIMA KEDUA DATA TERSEBUT
public function __construct($category, $filename)
{
$this->category = $category;
$this->filename = $filename;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
//KEMUDIAN KITA GUNAKAN PRODUCTIMPORT YANG MERUPAKAN CLASS YANG AKAN DIBUAT SELANJUTNYA
//IMPORT DATA EXCEL TADI YANG SUDAH DISIMPAN DI STORAGE, KEMUDIAN CONVERT MENJADI ARRAY
$files = (new ProductImport)->toArray(storage_path('app/public/uploads/' . $this->filename));
//KEMUDIAN LOOPING DATANYA
foreach ($files[0] as $row) {
// if ($row[4] != '' && filter_var($row[4], FILTER_VALIDATE_URL)) {
//FORMATTING URLNYA UNTUK MENGAMBIL FILE-NAMENYA BESERTA EXTENSION
//JADI PASTIKAN PADA TEMPLATE MASS UPLOADNYA NANTI PADA BAGIAN URL
//HARUS DIAKHIRI DENGAN NAMA FILE YANG LENGKAP DENGAN EXTENSION
$explodeURL = explode('/', $row[4]);
$explodeExtension = explode('.', end($explodeURL));
$filename = time() . Str::random(6) . '.' . end($explodeExtension);
//DOWNLOAD GAMBAR TERSEBUT DARI URL TERKAIT
file_put_contents(storage_path('app/public/products') . '/' . $filename, file_get_contents($row[4]));
//KEMUDIAN SIMPAN DATANYA DI DATABASE
Product::create([
'name' => $row[0],
'slug' => $row[0],
'category_id' => $this->category,
'description' => $row[1],
'price' => $row[2],
'weight' => $row[3],
'image' => $filename,
'status' => true
]);
// }
}
//JIKA PROSESNYA SUDAH SELESAI MAKA FILE YANG ADA DISTORAGE AKAN DIHAPUS
File::delete(storage_path('app/public/uploads/' . $this->filename));
}
}
Nah ada yang kurang nih, jadi class ProductImport
sudah digunakan pada code diatas, tapi sebenarnya file tersebut belum ada. Maka kita harus membuatnya dengan command
php artisan make:import ProductImport
Kemudian buka file ProductImport.php
yang berada didalam folder app/Imports
dan modifikasi menjadi
<?php
namespace App\Imports;
use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\Importable;
use Maatwebsite\Excel\Concerns\WithStartRow;
use Maatwebsite\Excel\Concerns\WithChunkReading;
class ProductImport implements WithStartRow, WithChunkReading
{
/**
* @param Collection $collection
*/
use Importable;
//JADI KITA BATAS DATA YANG AKAN DIGUNAKAN MULAI DARI BARIS KEDUA, KARENA BARIS PERTAMA DIGUNAKAN SEBAGAI HEADING AGAR MEMUDAHKAN ORANG YANG MENGISI DATA PADA FILE EXCEL
public function startRow(): int
{
return 2;
}
//KEMUDIAN KITA GUNAKAN chunkSize UNTUK MENGONTROL PENGGUNAAN MEMORY DENGAN MEMBATASI LOAD DATA DALAM SEKALI PROSES
public function chunkSize(): int
{
return 100;
}
}
Langkah terakhir adalah membuat table yang akan menampung data request yang sedang dalam antrian. Pada command line jalankan command
php artisan queue:table
php artisan migrate
Jangan lupa pada bagian .env
, ubah value QUEUE_CONNECTION
menjadi database
. Jika kamu menggunakan php artisan serve
, pastikan restart terlebih dahulu sebelum melakukan testing. Adapun format file Excelnya bisa di-download di Gihutb.
Setelah testing-nya berhasil dan data antrian telah masuk ke-table jobs
, maka eksekusi antrian tersebut dengan command
php artisan queue:work
Edit Data Product
Bagian yang tertinggal dari sebelumnya adalah fitur untuk mengedit data produk yang ada. Buka file ProductController.php
dan tambahkan method edit()
public function edit($id)
{
$product = Product::find($id); //AMBIL DATA PRODUK TERKAIT BERDASARKAN ID
$category = Category::orderBy('name', 'DESC')->get(); //AMBIL SEMUA DATA KATEGORI
return view('products.edit', compact('product', 'category')); //LOAD VIEW DAN PASSING DATANYA KE VIEW
}
Kemudian buat file edit.blade.php
dan tempatkan kedalam folder resources/views/products
. Tambahkan code berikut
@extends('layouts.admin')
@section('title')
<title>Edit Produk</title>
@endsection
@section('content')
<main class="main">
<ol class="breadcrumb">
<li class="breadcrumb-item">Home</li>
<li class="breadcrumb-item active">Product</li>
</ol>
<div class="container-fluid">
<div class="animated fadeIn">
<!-- PASTIKAN MENGIRIMKAN ID PADA ROUTE YANG DIGUNAKAN -->
<form action="{{ route('product.update', $product->id) }}" method="post" enctype="multipart/form-data" >
@csrf
<!-- KARENA UPDATE MAKA KITA GUNAKAN DIRECTIVE DIBAWAH INI -->
@method('PUT')
<!-- FORM INI SAMA DENGAN CREATE, YANG BERBEDA HANYA ADA TAMBAHKAN VALUE UNTUK MASING-MASING INPUTAN -->
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h4 class="card-title">Edit Produk</h4>
</div>
<div class="card-body">
<div class="form-group">
<label for="name">Nama Produk</label>
<input type="text" name="name" class="form-control" value="{{ $product->name }}" required>
<p class="text-danger">{{ $errors->first('name') }}</p>
</div>
<div class="form-group">
<label for="description">Deskripsi</label>
<textarea name="description" id="description" class="form-control">{{ $product->description }}</textarea>
<p class="text-danger">{{ $errors->first('description') }}</p>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-body">
<div class="form-group">
<label for="status">Status</label>
<select name="status" class="form-control" required>
<option value="1" {{ $product->status == '1' ? 'selected':'' }}>Publish</option>
<option value="0" {{ $product->status == '0' ? 'selected':'' }}>Draft</option>
</select>
<p class="text-danger">{{ $errors->first('status') }}</p>
</div>
<div class="form-group">
<label for="category_id">Kategori</label>
<select name="category_id" class="form-control">
<option value="">Pilih</option>
@foreach ($category as $row)
<option value="{{ $row->id }}" {{ $product->category_id == $row->id ? 'selected':'' }}>{{ $row->name }}</option>
@endforeach
</select>
<p class="text-danger">{{ $errors->first('category_id') }}</p>
</div>
<div class="form-group">
<label for="price">Harga</label>
<input type="number" name="price" class="form-control" value="{{ $product->price }}" required>
<p class="text-danger">{{ $errors->first('price') }}</p>
</div>
<div class="form-group">
<label for="weight">Berat</label>
<input type="number" name="weight" class="form-control" value="{{ $product->weight }}" required>
<p class="text-danger">{{ $errors->first('weight') }}</p>
</div>
<!-- GAMBAR TIDAK LAGI WAJIB, JIKA DIISI MAKA GAMBAR AKAN DIGANTI, JIKA DIBIARKAN KOSONG MAKA GAMBAR TIDAK AKAN DIUPDATE -->
<div class="form-group">
<label for="image">Foto Produk</label>
<br>
<!-- TAMPILKAN GAMBAR SAAT INI -->
<img src="{{ asset('storage/products/' . $product->image) }}" width="100px" height="100px" alt="{{ $product->name }}">
<hr>
<input type="file" name="image" class="form-control">
<p><strong>Biarkan kosong jika tidak ingin mengganti gambar</strong></p>
<p class="text-danger">{{ $errors->first('image') }}</p>
</div>
<div class="form-group">
<button class="btn btn-primary btn-sm">Update</button>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</main>
@endsection
@section('js')
<script src="https://cdn.ckeditor.com/4.13.0/standard/ckeditor.js"></script>
<script>
CKEDITOR.replace('description');
</script>
@endsection
Kembali ke file ProductController.php
, tambahkan method update()
untuk meng-handle proses pengubahan data produk.
public function update(Request $request, $id)
{
//VALIDASI DATA YANG DIKIRIM
$this->validate($request, [
'name' => 'required|string|max:100',
'description' => 'required',
'category_id' => 'required|exists:categories,id',
'price' => 'required|integer',
'weight' => 'required|integer',
'image' => 'nullable|image|mimes:png,jpeg,jpg' //IMAGE BISA NULLABLE
]);
$product = Product::find($id); //AMBIL DATA PRODUK YANG AKAN DIEDIT BERDASARKAN ID
$filename = $product->image; //SIMPAN SEMENTARA NAMA FILE IMAGE SAAT INI
//JIKA ADA FILE GAMBAR YANG DIKIRIM
if ($request->hasFile('image')) {
$file = $request->file('image');
$filename = time() . Str::slug($request->name) . '.' . $file->getClientOriginalExtension();
//MAKA UPLOAD FILE TERSEBUT
$file->storeAs('public/products', $filename);
//DAN HAPUS FILE GAMBAR YANG LAMA
File::delete(storage_path('app/public/products/' . $product->image));
}
//KEMUDIAN UPDATE PRODUK TERSEBUT
$product->update([
'name' => $request->name,
'description' => $request->description,
'category_id' => $request->category_id,
'price' => $request->price,
'weight' => $request->weight,
'image' => $filename
]);
return redirect(route('product.index'))->with(['success' => 'Data Produk Diperbaharui']);
}
Proses update produk berakhir disini karena cara kerjanya serupa dengan add new data hanya saja ditambahkan logic persoalan gambar yang boleh kosong.
[Issue] Tombol Edit Produk
Jadi pada artikel sebelumnya terjadi kesalahan link pada tombol edit produk dihalaman list products. Buka file index.blade.php
yang berada didalam folder resources/views/products
dan cukup modifikasi tombol edit-nya saja menjadi
<a href="{{ route('product.edit', $row->id) }}" class="btn btn-warning btn-sm">Edit</a>
Baca Juga: Aplikasi E-Commerce Laravel 6 #3: Management Category
Kesimpulan
Artikel ini cukup singkat tapi kita telah belajar hal baru yakni bagaimana membuat jadwal permintaan yang akan dieksekusi kemudian. Tujuannya adalah membebaskan user dari 'pekerjaan' menunggu proses tersebut selesai yang dimana bisa saja terjadi kesalahan eksternal seperti koneksi internet yang mati. Maka proses tersebut yang membutuhkan load-time yang cukup dipindahkan kesisi server untuk diproses.
Adapun dokumentasi code dari artikel ini bisa dilihat di Github.
Comments