Aplikasi E-Commerce Laravel 6 #4: Upload Products

Aplikasi E-Commerce Laravel 6 #4: Upload Products

Pendahuluan

Salah satu fitur yang menjadi denyut nadi dari aplikasi penjualan adalah manajemen produk, sehingga pada serial kali ini kita akan membahas bagaimana membuat fitur upload products pada aplikasi e-commerce menggunakan Laravel 6.

Schema yang diinginkan adalah user mengirimkan request dari halaman tambah produk, berupa informasi produk beserta gambar, kemudian data tersebut diolah untuk proses penyimpanan ke-database. Adapun gambarnya akan disimpan dalam bentuk file image kedalam storage aplikasi ini, sehingga yang disimpan kedalam database hanyalah nama file-nya saja.

Baca Juga: Aplikasi E-Commerce Laravel 6 #3: Management Category (CRUD)

Add New Column to Products Table

Ketika proses development berjalan, terjadi perubahan atau penambahan informasi produk, maka disinilah migration akan memainkan perannya lebih lanjut dalam mengontrol perubahan informasi database tersebut. Skenarionya user dapat mengatur status produk sebagai publish atau draft yang berfungsi untuk mengontrol apakah data produk terkait akan ditampilkan pada halaman penjualan atau tidak. Maka kita perlu menambahkan column atau field baru pada table products menggunakan Migration.

Pada command line, jalankan command:

php artisan make:migration add_field_status_to_products_table

Kemudian buka file Migration yang baru saja di-generate yakni xxxx_xx_xx_add_field_status_to_products_table.php yang berada didalam folder database/migrations dan modifikasi menjadi:

<?php

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

class AddFieldStatusToProductsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        //KITA PILIH DATABASE YANG AKAN DITAMBAHKAN FIELD BARU
        Schema::table('products', function (Blueprint $table) {
            //KEMUDIAN TAMBAHKAN FIELDNYA DENGAN TIPE BOOLEAN DAN DISIMPAN SETELAH WEIGHT
            $table->boolean('status')->default(true)->after('weight');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        //APABILA ROLLBACK ATAU REFRESH DIJALANKAN
        Schema::table('products', function (Blueprint $table) {
            //MAKA AKAN MENGHAPUS FIELD STATUS DARI TABLE PRODUCTS
            $table->dropColumn('status');
        });
    }
}

Jangan lupa untuk menerapkan migration diatas, jalankan command php artisan migrate.

Manage List of Products

Fitur ini akan menampilkan informasi produk yang telah ditambahkan kedalam database. Selain itu, juga dilengkapi dengan fitur pencarian agar memudahkan user mencari produk apa yang ingin dilihat. Adapun teknik yang akan digunakan hampir sama ketika membuat fitur untuk menampilkan data produk, hanya saja terdapat tambahan untuk query untuk mem-filter data jika terdapat parameter pencarian di-url.

Hal pertama yang akan dilakukan ada men-generate controller baru bernama ProductController. Pada command line, jalankan command

php artisan make:controller ProductController

Agar sesuai dengan gambar alur diatas, kita buat routing-nya terlebih dahulu. Buka file routes/web.php dan tambahkan code berikut setelah routing category.

Route::resource('product', 'ProductController');

Note: Penjelasan routing diatas sama dengan penjelasan pada artikel sebelumnya. Hanya berbeda value yakni product.

Selanjutnya adalah buka file ProductController.php dan modifikasi menjadi

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Product; //LOAD MODEL PRODUCT

class ProductController extends Controller
{
    public function index()
    {
        //BUAT QUERY MENGGUNAKAN MODEL PRODUCT, DENGAN MENGURUTKAN DATA BERDASARKAN CREATED_AT
        //KEMUDIAN LOAD TABLE YANG BERELASI MENGGUNAKAN EAGER LOADING WITH()
        //ADAPUN CATEGORY ADALAH NAMA FUNGSI YANG NNTINYA AKAN DITAMBAHKAN PADA PRODUCT MODEL
        $product = Product::with(['category'])->orderBy('created_at', 'DESC');
      
        //JIKA TERDAPAT PARAMETER PENCARIAN DI URL ATAU Q PADA URL TIDAK SAMA DENGAN KOSONG
        if (request()->q != '') {
            //MAKA LAKUKAN FILTERING DATA BERDASARKAN NAME DAN VALUENYA SESUAI DENGAN PENCARIAN YANG DILAKUKAN USER
            $product = $product->where('name', 'LIKE', '%' . request()->q . '%');
        }
        //TERAKHIR LOAD 10 DATA PER HALAMANNYA
        $product = $product->paginate(10);
        //LOAD VIEW INDEX.BLADE.PHP YANG BERADA DIDALAM FOLDER PRODUCTS
        //DAN PASSING VARIABLE $PRODUCT KE VIEW AGAR DAPAT DIGUNAKAN
        return view('products.index', compact('product'));
    }
}

Saatnya untuk men-handle data produk untuk kemudian ditampilkan. Buat file index.blade.php didalam folder resources/views/products dan tambahkan code

@extends('layouts.admin')

@section('title')
    <title>List Product</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">
            <div class="row">
                <div class="col-md-12">
                    <div class="card">
                        <div class="card-header">
                            <h4 class="card-title">
                                List Product
                              
                                <!-- BUAT TOMBOL UNTUK MENGARAHKAN KE HALAMAN ADD PRODUK -->
                                <a href="{{ route('product.create') }}" class="btn btn-primary btn-sm float-right">Tambah</a>
                            </h4>
                        </div>
                        <div class="card-body">
                            <!-- JIKA TERDAPAT FLASH SESSION, MAKA TAMPILAKAN -->
                            @if (session('success'))
                                <div class="alert alert-success">{{ session('success') }}</div>
                            @endif

                            @if (session('error'))
                                <div class="alert alert-danger">{{ session('error') }}</div>
                            @endif
                            <!-- JIKA TERDAPAT FLASH SESSION, MAKA TAMPILAKAN -->

                            <!-- BUAT FORM UNTUK PENCARIAN, METHODNYA ADALAH GET -->
                            <form action="{{ route('product.index') }}" method="get">
                                <div class="input-group mb-3 col-md-3 float-right">
                                    <!-- KEMUDIAN NAME-NYA ADALAH Q YANG AKAN MENAMPUNG DATA PENCARIAN -->
                                    <input type="text" name="q" class="form-control" placeholder="Cari..." value="{{ request()->q }}">
                                    <div class="input-group-append">
                                        <button class="btn btn-secondary" type="button">Cari</button>
                                    </div>
                                </div>
                            </form>
                          
                            <!-- TABLE UNTUK MENAMPILKAN DATA PRODUK -->
                            <div class="table-responsive">
                                <table class="table table-hover table-bordered">
                                    <thead>
                                        <tr>
                                            <th>#</th>
                                            <th>Produk</th>
                                            <th>Harga</th>
                                            <th>Created At</th>
                                            <th>Status</th>
                                            <th>Aksi</th>
                                        </tr>
                                    </thead>
                                    <tbody>
                                        <!-- LOOPING DATA TERSEBUT MENGGUNAKAN FORELSE -->
                                        <!-- ADAPUN PENJELASAN ADA PADA ARTIKEL SEBELUMNYA -->
                                        @forelse ($product as $row)
                                        <tr>
                                            <td>
                                                <!-- TAMPILKAN GAMBAR DARI FOLDER PUBLIC/STORAGE/PRODUCTS -->
                                                <img src="{{ asset('storage/products/' . $row->image) }}" width="100px" height="100px" alt="{{ $row->name }}">
                                            </td>
                                            <td>
                                                <strong>{{ $row->name }}</strong><br>
                                                <!-- ADAPUN NAMA KATEGORINYA DIAMBIL DARI HASIL RELASI PRODUK DAN KATEGORI -->
                                                <label>Kategori: <span class="badge badge-info">{{ $row->category->name }}</span></label><br>
                                                <label>Berat: <span class="badge badge-info">{{ $row->weight }} gr</span></label>
                                            </td>
                                            <td>Rp {{ number_format($row->price) }}</td>
                                            <td>{{ $row->created_at->format('d-m-Y') }}</td>
                                            
                                            <!-- KARENA BERISI HTML MAKA KITA GUNAKAN { !! UNTUK MENCETAK DATA -->
                                            <td>{!! $row->status_label !!}</td>
                                            <td>
                                                <!-- FORM UNTUK MENGHAPUS DATA PRODUK -->
                                                <form action="{{ route('product.destroy', $row->id) }}" method="post">
                                                    @csrf
                                                    @method('DELETE')
                                                    <a href="{{ route('category.edit', $row->id) }}" class="btn btn-warning btn-sm">Edit</a>
                                                    <button class="btn btn-danger btn-sm">Hapus</button>
                                                </form>
                                            </td>
                                        </tr>
                                        @empty
                                        <tr>
                                            <td colspan="5" class="text-center">Tidak ada data</td>
                                        </tr>
                                        @endforelse
                                    </tbody>
                                </table>
                            </div>
                            <!-- MEMBUAT LINK PAGINASI JIKA ADA -->
                            {!! $product->links() !!}
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</main>
@endsection

Jangan lupa untuk membuat fungsi relasi antar table products dan categories. Buka file Product.php dan tambahkan method dibawah ini

//INI ADALAH ACCESSOR, JADI KITA MEMBUAT KOLOM BARU BERNAMA STATUS_LABEL
//KOLOM TERSEBUT DIHASILKAN OLEH ACCESSOR, MESKIPUN FIELD TERSEBUT TIDAK ADA DITABLE PRODUCTS
//AKAN TETAPI AKAN DISERTAKAN PADA HASIL QUERY
public function getStatusLabelAttribute()
{
    //ADAPUN VALUENYA AKAN MENCETAK HTML BERDASARKAN VALUE DARI FIELD STATUS
    if ($this->status == 0) {
        return '<span class="badge badge-secondary">Draft</span>';
    }
    return '<span class="badge badge-success">Aktif</span>';
}

//FUNGSI YANG MENG-HANDLE RELASI KE TABLE CATEGORY
public function category()
{
    return $this->belongsTo(Category::class);
}

Agar bisa diakses dari sidebar menu navigasi, buka file sidebar.blade.php yang berada didalam folder resources/views/layouts/module dan tambahkan tag berikut setelah menu kategori.

<li class="nav-item">
    <a class="nav-link" href="{{ route('product.index') }}">
        <i class="nav-icon icon-drop"></i> Produk
    </a>
</li>

Sehingga hasil yang akan diperoleh ketika mengakses halaman http://localhost:8000/administrator/product adalah sebagaimana gambar berikut

Form Add New Product

Fitur ini akan mengajarkan kita hal baru yakni bagaimana meng-handle file di Laravel yang kemudian disimpan pada storage internal aplikasi. Jadi sekali lagi, file-nya tidak akan disimpan ke-database melainkan kedalam storage layaknya asset lainnya seperti css dan js.

Tugas pertama kita pada bagian ini adalah membuat form-nya terlebih dahulu. Karena routing-nya sudah di-generate oleh Route::resource, maka kita hanya perlu melangkah ke bagian selanjutnya yakni membuat method create(). Buka file ProductController.php dan tambahkan code

public function create()
{
    //QUERY UNTUK MENGAMBIL SEMUA DATA CATEGORY
    $category = Category::orderBy('name', 'DESC')->get();
    //LOAD VIEW create.blade.php` YANG BERADA DIDALAM FOLDER PRODUCTS
    //DAN PASSING DATA CATEGORY
    return view('products.create', compact('category'));
}

Jangan lupa tambahkan use statement pada file yang sama

use App\Category;

Kemudian buat file create.blade.php didalam folder resources/views/products/ dan tambahkan code

@extends('layouts.admin')

@section('title')
    <title>Tambah 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">
          
          	<!-- TAMBAHKAN ENCTYPE="" KETIKA MENGIRIMKAN FILE PADA FORM -->
            <form action="{{ route('product.store') }}" method="post" enctype="multipart/form-data" >
                @csrf
                <div class="row">
                    <div class="col-md-8">
                        <div class="card">
                            <div class="card-header">
                                <h4 class="card-title">Tambah 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="{{ old('name') }}" required>
                                    <p class="text-danger">{{ $errors->first('name') }}</p>
                                </div>
                                <div class="form-group">
                                    <label for="description">Deskripsi</label>
                                  
                                    <!-- TAMBAHKAN ID YANG NNTINYA DIGUNAKAN UTK MENGHUBUNGKAN DENGAN CKEDITOR -->
                                    <textarea name="description" id="description" class="form-control">{{ old('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" {{ old('status') == '1' ? 'selected':'' }}>Publish</option>
                                        <option value="0" {{ old('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>
                                    
                                    <!-- DATA KATEGORI DIGUNAKAN DISINI, SEHINGGA SETIAP PRODUK USER BISA MEMILIH KATEGORINYA -->
                                    <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="price">Harga</label>
                                    <input type="number" name="price" class="form-control" value="{{ old('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="{{ old('weight') }}" required>
                                    <p class="text-danger">{{ $errors->first('weight') }}</p>
                                </div>
                                <div class="form-group">
                                    <label for="image">Foto Produk</label>
                                    <input type="file" name="image" class="form-control" value="{{ old('image') }}" required>
                                    <p class="text-danger">{{ $errors->first('image') }}</p>
                                </div>
                                <div class="form-group">
                                    <button class="btn btn-primary btn-sm">Tambah</button>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </form>
        </div>
    </div>
</main>
@endsection

<!-- PADA ADMIN LAYOUTS, TERDAPAT YIELD JS YANG BERARTI KITA BISA MEMBUAT SECTION JS UNTUK MENAMBAHKAN SCRIPT JS JIKA DIPERLUKAN -->
@section('js')
    <!-- LOAD CKEDITOR -->
    <script src="https://cdn.ckeditor.com/4.13.0/standard/ckeditor.js"></script>
    <script>
        //TERAPKAN CKEDITOR PADA TEXTAREA DENGAN ID DESCRIPTION
        CKEDITOR.replace('description');
    </script>
@endsection

Form-nya sudah tersedia, maka langkah selanjutnya adalah meng-handle data yang dikirim untuk kemudian diproses lebih lanjut. Adapun flow-nya sebagai berikut

Form request akan mengirimkan permintaan dengan method POST pada url /administrator/product dimana route ini terhubung dengan method store() pada controller sebagaimana penjelasan dari artikel sebelumnya. Buka file ProductController dan tambahkan code berikut

public function store(Request $request)
{
    //VALIDASI REQUESTNYA
    $this->validate($request, [
        'name' => 'required|string|max:100',
        'description' => 'required',
        'category_id' => 'required|exists:categories,id', //CATEGORY_ID KITA CEK HARUS ADA DI TABLE CATEGORIES DENGAN FIELD ID
        'price' => 'required|integer',
        'weight' => 'required|integer',
        'image' => 'required|image|mimes:png,jpeg,jpg' //GAMBAR DIVALIDASI HARUS BERTIPE PNG,JPG DAN JPEG
    ]);

    //JIKA FILENYA ADA
    if ($request->hasFile('image')) {
        //MAKA KITA SIMPAN SEMENTARA FILE TERSEBUT KEDALAM VARIABLE FILE
        $file = $request->file('image');
        //KEMUDIAN NAMA FILENYA KITA BUAT CUSTOMER DENGAN PERPADUAN TIME DAN SLUG DARI NAMA PRODUK. ADAPUN EXTENSIONNYA KITA GUNAKAN BAWAAN FILE TERSEBUT
        $filename = time() . Str::slug($request->name) . '.' . $file->getClientOriginalExtension();
        //SIMPAN FILENYA KEDALAM FOLDER PUBLIC/PRODUCTS, DAN PARAMETER KEDUA ADALAH NAMA CUSTOM UNTUK FILE TERSEBUT
        $file->storeAs('public/products', $filename);

        //SETELAH FILE TERSEBUT DISIMPAN, KITA SIMPAN INFORMASI PRODUKNYA KEDALAM DATABASE
        $product = Product::create([
            'name' => $request->name,
            'slug' => $request->name,
            'category_id' => $request->category_id,
            'description' => $request->description,
            'image' => $filename, //PASTIKAN MENGGUNAKAN VARIABLE FILENAM YANG HANYA BERISI NAMA FILE SAJA (STRING)
            'price' => $request->price,
            'weight' => $request->weight,
            'status' => $request->status
        ]);
        //JIKA SUDAH MAKA REDIRECT KE LIST PRODUK
        return redirect(route('product.index'))->with(['success' => 'Produk Baru Ditambahkan']);
    }
}

Note: Parameter pertama public/products bukan merujuk pada folder tersebut. Akan tetapi jika kita lihat config/filesystems.php dimana key default menggunakan parameter local sebagai default disk-nya. Kemudian scroll kebawah didalam file yang sama, pada bagian disks bisa kita lihat key local root foldernya merujuk ke storage/app. Sehingga, ketika kita menggunakan public/products pada method storeAs(), maka secara otomatis file tersebut akan disimpan kedalam folder storage/app/public/products.

Masih dengan file yang sama, jangan lupa untuk menambahkan use statement:

use Illuminate\Support\Str;

Bagian yang penting adalah mengizinkan fungsi mass assignment untuk menyimpan kedalam model Product. Jika sebelumnya kita menggunakan $fillable, maka kali ini kita akan menggunakan cara yang sebaliknya. Buka file Product.php dan tambahkan code

//JIKA FILLABLE AKAN MENGIZINKAN FIELD APA SAJA YANG ADA DIDALAM ARRAYNYA
//MAKA GUARDED AKAN MEMBLOK FIELD APA SAJA YANG ADA DIDALAM ARRAY-NYA
//JADI APABILA FIELDNYA BANYAK MAKA KITA BISA MANFAATKAN DENGAN HANYA MENULISKAN ARRAY KOSONG
//YANG BERARTI TIDAK ADA FIELD YANG DIBLOCK SEHINGGA SEMUA FIELD TERSEBUT SUDAH DIIZINAKAN
//HAL INI MEMUDAHKAN KITA KARENA TIDAK PERLU MENULISKANNYA SATU PERSATU
protected $guarded = [];

//SEDANGKAN INI ADALAH MUTATORS, PENJELASANNYA SAMA DENGAN ARTIKEL SEBELUMNYA
public function setSlugAttribute($value)
{
    $this->attributes['slug'] = Str::slug($value); 
}

Jangan lupa juga untuk menambahkan use statement pada model diatas

use Illuminate\Support\Str;

Sebagai penutup dari sub-bab ini adalah dengan membuat symlink agar file yang ada di-storage bisa diakses secara publik. Pada command line, jalankan command

php artisan storage:link

Delete Product & Image

Fitur ini hampir sama dengan cara kerja dari proses delete kategori, yang membedakan karena adanya tambahkan fungsi untuk menghapus file image dari produk terkait. Jadi schema-nya adalah ketika user menekan tombol hapus pada salah satu produk, maka fungsi tersebut akan mengirimkan request berdasarkan id untuk menghapus data terkait. Proses-nya adalah Model akan melakukan query untuk mengambil data tersebut, kemudian kita hapus image terkait terlebih dahulu dan diteruskan dengan penghapusan data produk dari database.

Tombol delete beserta form-nya sudah dikerjakan ketika fitur manage list products dibuat, juga tidak terlupakan bahwa routing-nya juga sudah ada. Maka tugas kita hanyalah meng-handle prosesnya saja. Buka file ProductController.php dan tambahkan method

public function destroy($id)
{
    $product = Product::find($id); //QUERY UNTUK MENGAMBIL DATA PRODUK BERDASARKAN ID
    //HAPUS FILE IMAGE DARI STORAGE PATH DIIKUTI DENGNA NAMA IMAGE YANG DIAMBIL DARI DATABASE
    File::delete(storage_path('app/public/products/' . $product->image));
    //KEMUDIAN HAPUS DATA PRODUK DARI DATABASE
    $product->delete();
    //DAN REDIRECT KE HALAMAN LIST PRODUK
    return redirect(route('product.index'))->with(['success' => 'Produk Sudah Dihapus']);
}

Jangan lupa dengan file yang sama, tambahkan use statement

use File;

[ISSUE] Validate Delete Category

Jika sebelumnya kita membuat fungsi validasi sebelum menghapus kategori, dimana kondisinya jika kategori tersebut tidak memiliki anak kategori maka kategori terkait akan dihapus. Pada bagian ini akan ditambahkan kondisi lainnya, yakni sebelum menghapus kategori tersebut juga dilakukan pengecekan jika kategori terkait belum digunakan oleh produk manapun maka kategori tersebut diperbolehkan untuk dihapus.

Pertama, buat relasinya antara Category.php dan Product.php. Buka file Category.php dan tambahkan code

public function product()
{
    //JENIS RELASINYA ADALAH ONE TO MANY, YANG BERARTI KATEGORI INI BISA DIGUNAKAN OLEH BANYAK PRODUK
    return $this->hasMany(Product::class);
}

Kemudian buka file CategoryController.php dan modifikasi method destroy() menjadi

public function destroy($id)
{
    //TAMBAHKAN product KEDALAM ARRAY WITHCOUNT()
    //FUNGSI INI AKAN MEMBENTUK FIELD BARU YANG BERNAMA product_count
    $category = Category::withCount(['child', 'product'])->find($id);
    //KEMUDIAN PADA IF STATEMENTNYA KITA CEK JUGA JIKA = 0
    if ($category->child_count == 0 && $category->product_count == 0) {
        $category->delete();
        return redirect(route('category.index'))->with(['success' => 'Kategori Dihapus!']);
    }
    return redirect(route('category.index'))->with(['error' => 'Kategori Ini Memiliki Anak Kategori!']);
}

Baca Juga: Aplikasi E-Commerce Laravel 6 #2: Templating & Authentication

Kesimpulan

Artikel ini sudah cukup panjang, maka fitur edit data produk akan dibahas pada artikel selanjutnya. Didalam artikel ini kita telah belajar banyak hal, diantaranya: bagaimana mengupload gambar di Laravel, pengulangan bagaimana membuat fungsi crud di Laravel, juga bagaimana menghapus gambar di Laravel dan banyak lagi hal hal menarik serta cara lainnya dalam menggunakan Laravel.

Adapun dokumentasi code bisa kamu lihat di Github.

Category:
Share:

Comments