setelah berhasil mempraktekan login dan register, sekarang saatnya untuk membuat aplikasi ini menjadi multi user
multi user ini maksudnya setiap user akan memiliki data produknya masing-masing, jadi user satu dan user lainnya berbeda data produknya
berikut spesifikasi kode yang akan dibuat
- Proteksi Proses CRUD dengan Hash
- Memisahkan tampilan Data setiap user
- Mengubah Tampilan Produk diHomepage
Persiapan
karena ini melanjutkan proses praktek sebelumnya, maka kita gunakan file yang sebelumnya berhasil dipraktein yaitu membuat login dan register
Mengubah Table Product
kita akan mengubah table product dengan menambahkan kolom baru yaitu id_user
kolom baru ini berguna untuk memberi identitas setiap data produk yang ada tiap usernya, agar nantinya ketika data dipanggil oleh setiap user berbeda
buat migration baru dengan perintah
php spark make:migration AlterProduct
buka file migration AlterProduct yang terletak di 'app/Database/Migrations/DATETIME_AlterProduct.php' dan ganti kodenya dengan kode dibawah ini
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AlterProduct extends Migration
{
public function up()
{
$this->forge->addColumn('product', [
'id_user' => [
'type' => 'INT',
'constraint' => 100,
'after' => 'id'
]
]);
}
public function down()
{
$this->forge->dropColumn('product', 'id_user');
}
}
jalankahn migrasinya dengan mengetikan perintah spark
php spark migrate
Mengubah Controller Product
buka file controller product yang terletak di 'app/Controllers/Product.php', kemudian ubah semua kodenya menjadi seperti dibawah ini
<?php
namespace App\Controllers;
use App\Controllers\BaseController;
class Product extends BaseController
{
public function index()
{
$data['title'] = 'Product';
return view('product', $data);
}
public function read()
{
$productModel = new \App\Models\Product();
$products = $productModel->getDataForBootstrapTable($this->request);
return $this->response->setJSON($products);
}
private function _post_product($is_update = false)
{
// validate input text
$validationRule = [
'name' => [
'rules' => 'required'
],
'category' => [
'rules' => 'required'
],
'price' => [
'rules' => 'required'
]
];
if (!$this->validate($validationRule)) {
$error = $this->validator->getErrors();
$error_val = array_values($error);
die(json_encode([
'status' => false,
'response' => $error_val[0]
]));
}
$data['name'] = $this->request->getPost('name');
$data['category'] = $this->request->getPost('category');
$data['price'] = $this->request->getPost('price');
// ======== photo handle
// if create and not have photo
if (!$is_update AND !$this->request->getFile('photo')->isValid()) {
die(json_encode([
'status' => false,
'response' => 'photo required'
]));
}
// check new photo exist
$photo = $this->request->getFile('photo');
if ($photo->isValid()) {
// validate input file
$validationRule = [
'photo' => [
'rules' => 'uploaded[photo]'
. '|is_image[photo]'
. '|mime_in[photo,image/jpg,image/jpeg,image/gif,image/png,image/webp]'
. '|max_size[photo,100]'
. '|max_dims[photo,1024,768]'
]
];
if (!$this->validate($validationRule)) {
$error = $this->validator->getErrors();
$error_val = array_values($error);
die(json_encode([
'status' => false,
'response' => $error_val[0]
]));
}
$file_name = $data['name'].'.'.$photo->getClientExtension();
$dir_upload = './uploads/';
$file_des = $dir_upload.$file_name;
// if update
if ($is_update) {
// delete previous photo
$prev_photo = $this->request->getPost('previous_photo');
if (file_exists($dir_upload.$prev_photo)) {
unlink($dir_upload.$prev_photo);
}
}
// then upload
$photo->move('./uploads/', $file_name);
$data['photo'] = $file_name;
}
// ======== photo handle
return $data;
}
public function create()
{
$data = $this->_post_product();
// insert id_user
$data['id_user'] = session('auth')['id'];
$productModel = new \App\Models\Product();
$productModel->insert($data);
return $this->response->setJSON([
'status' => true,
'response' => 'Success create data '.$data['name']
]);
}
private function _hash_handle()
{
// load helper
helper('aeshash');
return aeshash('dec', $this->request->getPost('hash') , session('auth')['id'] );
}
public function edit()
{
$id = $this->_hash_handle();
$productModel = new \App\Models\Product();
$product = $productModel->select('name,category,price,photo')->where('id_user', session('auth')['id'])->find($id);
// check product
if (!$product) {
return $this->response->setJSON([
'status' => false,
'response' => 'product invalid, are you tester ?'
]);
}
// build hash
$product['hash'] = $this->request->getPost('hash');
return $this->response->setJSON([
'status' => true,
'response' => $product
]);
}
public function update()
{
$id = $this->_hash_handle();
$data = $this->_post_product($id);
$productModel = new \App\Models\Product();
$productModel->where('id_user', session('auth')['id'])->update($id, $data);
return $this->response->setJSON([
'status' => true,
'response' => 'Success update data '.$data['name']
]);
}
public function delete()
{
$id = $this->_hash_handle();
$productModel = new \App\Models\Product();
// read first
$product = $productModel->where('id_user', session('auth')['id'])->select('photo')->find($id);
// check product
if (!$product) {
return $this->response->setJSON([
'status' => false,
'response' => 'product invalid, are you tester ?'
]);
}
// then delete
if ($productModel->delete($id) AND file_exists('./uploads/'.$product['photo'])) {
// delete photo
unlink('./uploads/'.$product['photo']);
}
return $this->response->setJSON([
'status' => true,
'response' => 'Success delete data '.$id
]);
}
public function delete_batch()
{
$hash_ids = $this->request->getPost('ids');
$hash_ids_array = explode("','", $hash_ids);
$count = count($hash_ids_array);
// dec hash_ids_hash > ids_array
helper('aeshash');
$ids_array = [];
foreach ($hash_ids_array as $hash) {
$dec = aeshash('dec', $hash , session('auth')['id'] );
if ($dec) {
// only valid hash insert to ids_array
$ids_array[] = $dec;
}
}
// read model
$productModel = new \App\Models\Product();
// read first
$products = $productModel->where('id_user', session('auth')['id'])->select('id,photo')->find($ids_array);
// check products
if (!$products) {
return $this->response->setJSON([
'status' => false,
'response' => 'product invalid, are you tester ?'
]);
}
// then delete one by one
foreach ($products as $product) {
if ($productModel->delete($product['id']) AND file_exists('./uploads/'.$product['photo'])) {
// delete photo
unlink('./uploads/'.$product['photo']);
}
}
return $this->response->setJSON([
'status' => true,
'response' => 'Success '. $count .' delete data '
]);
}
}
Mengubah Model Product
buka file model product terletak di 'app/Models/Product.php', ganti kodenya menjadi seperti dibawah ini
<?php
namespace App\Models;
use CodeIgniter\Model;
class Product extends Model
{
protected $table = 'product';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $returnType = 'array';
protected $protectFields = false;
// Dates
protected $useTimestamps = true;
protected $dateFormat = 'datetime';
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
public function getData()
{
$products = $this->select('
product.id,
product.name,
product.category,
product.price,
product.photo,
user.username
')
->join('user', 'product.id_user = user.id')
->orderBy('id','DESC')->findAll();
// load helper
helper('number');
// build data
$data = [];
foreach ($products as $product) {
$data[] = array(
'id' => $product['id'],
'name' => $product['name'],
'category' => $product['category'],
'price' => number_to_currency($product['price'], "IDR", "id", 0),
'photo' => base_url('uploads/'.$product['photo']),
'owner' => $product['username'],
);
}
return $data;
}
public function getDataForBootstrapTable($request)
{
$builder = $this->select('id, name, price')->where('id_user', session('auth')['id']);
// search query
$builder->like('name', $request->getGet('search'));
// sort query
$builder->orderBy($request->getGet('sort'), $request->getGet('order'));
// paging query
$builder->limit($request->getGet('limit'), $request->getGet('offset'));
$total = $builder->countAllResults(false); // set false for not reset query
$products = $builder->get()->getResultArray();
$total_filter = count($products);
// load helper
helper('number');
helper('aeshash');
// build data
$data = [];
foreach ($products as $product) {
$data[] = array(
'hash' => aeshash('enc', $product['id'] , session('auth')['id'] ),
'name' => $product['name'],
'price' => number_to_currency($product['price'], "IDR", "id", 0),
);
}
return [
'total' => $total,
'totalNotFiltered' => $total_filter,
'rows' => $data
];
}
}
Ubah View
Ubah View Homepage
buka file view homepage terletak di 'app/Views/homepage.php', ganti kodenya menjadi seperti dibawah ini
<?= $this->extend('_layout') ?>
<?= $this->section('content') ?>
<main class="py-5 my-auto">
<div class="container">
<?php if (!$products): ?>
<div class="text-center fs-3">
There are no products to display yet
</div>
<?php endif ?>
<div class="row row-cols-2 row-cols-sm-2 row-cols-md-4 g-3">
<?php foreach ($products as $product): ?>
<!-- make card same height with d-flex align-items-stretch -->
<div class="col d-flex align-items-stretch">
<div class="card shadow-sm">
<div style="position: relative;">
<img class="bd-placeholder-img card-img-top" src="<?= $product['photo'] ?>">
<span class="badge bg-primary" style="position: absolute;right: 5px;top: 10px;">
<i class="bi bi-hash" style="font-size:10px"></i>
<?= $product['category'] ?>
</span>
</div>
<!-- make card same height with d-flex flex-column -->
<div class="card-body d-flex flex-column">
<h5 class="card-title pb-2">
<a class="text-decoration-none" href="#">
<?= $product['name'] ?>
</a>
</h5>
<!-- make this element always on bottom when height is not same -->
<div class="d-flex justify-content-between align-items-center mb-1 mt-auto">
<small class="text-muted"><?= $product['price'] ?></small>
<div>
<i class="bi bi-person-circle"></i> <?= $product['owner'] ?>
</div>
</div>
</div>
</div>
</div>
<?php endforeach ?>
</div>
</div>
</main>
<?= $this->endSection() ?>
Ubah View _layout/nav.php
ganti view nav.php yang terletak di 'app/Views/_layout/nav.php' dengan kode dibawah ini
<nav class="navbar navbar-expand-md mb-4">
<div class="container">
<span class="navbar-brand">Store</span>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav me-auto mb-2 mb-md-0">
<li class="nav-item">
<a class="nav-link" aria-current="page" href="<?= base_url() ?>">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="<?= base_url('product') ?>">Product</a>
</li>
</ul>
<ul class="navbar-nav mb-2 mb-md-0 d-flex">
<?php if (!session()->auth): ?>
<li class="nav-item">
<a class="nav-link" aria-current="page" href="<?= base_url('login') ?>">Login</a>
</li>
<li class="nav-item">
<a class="nav-link" href="<?= base_url('register') ?>">Register</a>
</li>
<?php else: ?>
<li class="nav-item">
<span class="nav-link">
Hello <b><?= session('auth')['username']; ?></b>
</span>
</li>
<li class="nav-item">
<a class="nav-link btn btn-outline-danger me-2" aria-current="page" href="<?= base_url('logout') ?>">Logout</a>
</li>
<?php endif ?>
</ul>
</div>
</div>
</nav>
Ubah View product.php
ganti view product.php yang terletak di 'app/Views/product.php' dengan kode dibawah ini
<?= $this->extend('_layout') ?>
<?= $this->section('css') ?>
<!-- bootstrap-table -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.20.2/bootstrap-table.min.css" integrity="sha512-HIPiLbxNKmx+x+VFnDHICgl1nbRzW3tzBPvoeX0mq9dWP9H1ZGMRPXfYsHhcJS1na58bzUpbbfpQ/n4SIL7Tlw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<main class="py-5">
<div class="container">
<div class="card">
<div class="card-header bg-light">
<div class="row">
<div class="col-auto">
<h1 class="fs-4">
Data Product
</h1>
</div>
<div class="col">
<button id="create-product" class="btn btn-outline-primary btn-sm">
<i class="bi bi-plus"></i> Create
</button>
</div>
</div>
</div>
<div class="card-body">
<div id="toolbar" class="btn-group">
<button class="btn btn-danger btn-md" id="delete" disabled><i class="bi bi-trash"></i></button>
</div>
<table
id="table"
data-toolbar="#toolbar"
data-search="true"
data-show-refresh="true"
data-show-columns="false"
data-minimum-count-columns="2"
data-show-pagination-switch="false"
data-detail-view="false"
data-detail-formatter="detailFormatter"
data-pagination="true"
data-page-list="[10, 25, 50, 100, all]"
data-click-to-select="false"
data-id-field="id"
data-height="auto"
data-url="<?= base_url('product/read') ?>"
data-side-pagination="server"
data-response-handler="responseHandler"
data-sort-name="id"
data-sort-order="desc"
></table>
</div>
</div>
</div>
</main>
<?= $this->endSection() ?>
<?= $this->section('js') ?>
<!-- bootstrap-table -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.20.2/bootstrap-table.min.js" integrity="sha512-9KY1w0S1bRPqvsNIxj3XovwFzZ7bOFG8u0K1LByeMVrzYhLu3v7sFRAlwfhBVFsHRiuMc6shv1lRLCTInwqxNg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
let $table = $('#table'),
$delete = $('#delete'),
selections = [];
$table.bootstrapTable('destroy').bootstrapTable({
responseHandler: function(res) {
$.each(res.rows, function (i, row) {
row.state = $.inArray(row.hash, selections) !== -1
})
return res
},
detailFormatter: function(index, row) {
var html = []
$.each(row, function (key, value) {
html.push('<p><b>' + key + ':</b> ' + value + '</p>')
})
return html.join('')
},
columns: [
{
field: 'state',
checkbox: true,
align: 'center',
valign: 'middle'
},
{
title: 'Name',
field: 'name',
align: 'left',
valign: 'middle',
sortable: true,
},
{
title: 'Price',
field: 'price',
align: 'left',
valign: 'middle',
sortable: true,
},
{
title: 'Action',
align: 'right',
valign: 'middle',
width: 100,
formatter: function(value, row, index) {
var html = '';
html += `<button data-hash="${row.hash}" class='btn btn-primary btn-sm me-1 action-edit'>
<i class='bi bi-pencil'></i>
</button>`;
html += `<button data-name="${row.name}" data-hash="${row.hash}" class='btn btn-danger btn-sm action-delete'>
<i class='bi bi-trash'></i>
</button>`;
return html;
}
}
]
});
$table.on('load-success.bs.table', function(){
/**
* Edit Product
*/
$(".action-edit").on('click', function(){
const hash = $(this).data('hash');
var loading_dialog = bootbox.dialog({
message: '<p class="text-center mb-0">Reading Data, Please Wait...</p>',
centerVertical: true,
closeButton: false,
size: 'medium',
});
loading_dialog.init(function(){
$.post(base_url + `/product/edit`, {hash : hash})
.done(function(data){
loading_dialog.modal('hide');
if (data.status) {
// call bootbox
let form_html = '';
form_html += `
<form id="form-product" enctype="multipart/form-data">
<div class="mb-3">
<label class="form-label text-capitalize">name</label>
<input value="${data.response.name}" name="name" type="text" class="form-control">
</div>
<div class="mb-3">
<label class="form-label text-capitalize">category</label>
<input value="${data.response.category}" name="category" type="text" class="form-control">
</div>
<div class="mb-3">
<label class="form-label text-capitalize">price</label>
<input value="${data.response.price}" name="price" type="number" class="form-control">
</div>
<div class="mb-3">
<label class="form-label text-capitalize">new photo</label>
<input value="${data.response.photo}" name="previous_photo" type="hidden" class="form-control">
<input name="photo" type="file" class="form-control">
</div>
<input value="${data.response.hash}" name="hash" type="hidden" class="form-control">
<button type="submit" class="btn btn-outline-primary btn-submit">Submit</button>
</form>
`;
var dialog = bootbox.dialog({
title: `Edit Product ${data.response.name}`,
message: form_html,
centerVertical: true,
closeButton: true,
size: 'medium',
onShown : function(){
$("input[name=name]",$("#form-product")).focus();
formProduct(dialog, 'update');
}
});
}else{
alert(data.response);
}
}).fail(function(xhr, statusText, errorThrown) {
alert(xhr.responseText);
});
});
})
/**
* Delete Product
*/
$(".action-delete").on('click', function(){
const hash = $(this).data('hash'),
name = $(this).data('name');
// call bootbox
let dialog = bootbox.confirm({
centerVertical: true,
closeButton: false,
title: 'Confirm Delete',
message: `Are you sure want to delete data ${name}`,
buttons: {
confirm: {
label: '<i class="bi bi-check"></i> Yes',
className: 'btn-primary'
},
cancel: {
label: '<i class="bi bi-x"></i> No',
className: 'btn-danger'
}
},
callback: function (result) {
if (result) {
// animation
$(".bootbox-accept, .bootbox-cancel").prop("disabled",true);
$(".bootbox-accept").html($(".bootbox-accept").html() + xsetting.spinner);
let buttonspinner = $(".button-spinner");
$.post(base_url + `/product/delete`, { hash: hash }, function(data) {}, 'json')
.done(function(data){
// animation
$(".bootbox-accept, .bootbox-cancel").prop("disabled",false);
buttonspinner.remove();
if (data.status) {
$table.bootstrapTable('refresh');
dialog.modal('hide'); // hide modal after get success response
}
})
.fail(function(xhr, statusText, errorThrown) {
alert(xhr.responseText);
// animation
$(".bootbox-accept, .bootbox-cancel").prop("disabled",false);
buttonspinner.remove();
});
// prevent hide modal
return false;
}
}
});
});
// for checkbox
$table.on('check.bs.table uncheck.bs.table ' + 'check-all.bs.table uncheck-all.bs.table', function () {
$delete.prop('disabled', !$table.bootstrapTable('getSelections').length);
});
})
/**
* Delete Multiple
*/
$delete.on('click', function(){
var ids = $.map($table.bootstrapTable('getSelections'), function (row) {
return row.hash
});
// validate
if (ids.length < 1) {return false;}
// convert
ids = ids.join("','");
let dialog = bootbox.confirm({
centerVertical: true,
closeButton: false,
title: `Confirm Batch Delete`,
message: `Are you sure want to delete selected data`,
buttons: {
confirm: {
label: '<i class="bi bi-check"></i> Yes',
className: 'btn-primary'
},
cancel: {
label: '<i class="bi bi-x"></i> No',
className: 'btn-danger'
}
},
callback: function (result) {
if (result) {
// animation
$(".bootbox-accept, .bootbox-cancel").prop("disabled",true);
$(".bootbox-accept").html($(".bootbox-accept").html() + xsetting.spinner);
let buttonspinner = $(".button-spinner");
$.post(base_url + `/product/delete_batch`, { ids:ids }, function(data) {}, 'json')
.done(function(data){
// animation
$(".bootbox-accept, .bootbox-cancel").prop("disabled",false);
buttonspinner.remove();
dialog.modal('hide');
if (data.status) {
$table.bootstrapTable('refresh');
}
})
.fail(function(xhr, statusText, errorThrown) {
alert(xhr.responseText);
// animation
$(".bootbox-accept, .bootbox-cancel").prop("disabled",false);
buttonspinner.remove();
dialog.modal('hide');
});
}
return false;
}
});
});
</script>
<script>
/**
* Create Product
*/
$("#create-product").on("click",function(e){
const button = $(this);
let form_html = '';
form_html += `
<form id="form-product" enctype="multipart/form-data">
<div class="mb-3">
<label class="form-label text-capitalize">name</label>
<input name="name" type="text" class="form-control">
</div>
<div class="mb-3">
<label class="form-label text-capitalize">category</label>
<input name="category" type="text" class="form-control">
</div>
<div class="mb-3">
<label class="form-label text-capitalize">price</label>
<input name="price" type="number" class="form-control">
</div>
<div class="mb-3">
<label class="form-label text-capitalize">photo</label>
<input name="photo" type="file" class="form-control">
</div>
<button type="submit" class="btn btn-outline-primary btn-submit">Submit</button>
</form>
`;
var dialog = bootbox.dialog({
title: `Create New Product`,
message: form_html,
centerVertical: true,
closeButton: true,
size: 'medium',
onShown : function(){
$("input[name=name]",$("#form-product")).focus();
formProduct(dialog, 'create');
}
});
});
function formProduct(dialog,action) {
let form = $("#form-product");
form.on("submit",function(e){
e.preventDefault();
// animation
$("input", form).prop("readonly",true);
$(".btn-submit").prop("disabled",true);
$(".btn-submit").html($(".btn-submit").html() + xsetting.spinner);
$(".bootbox-close-button").hide();
let buttonspinner = $(".button-spinner");
$.ajax({
url: base_url + `/product/` + action,
type: "POST",
data: new FormData($(this)[0]),
dataType: "json",
mimeTypes:"multipart/form-data",
contentType: false,
cache: false,
processData: false,
success: function(data){
// animation
$("input", form).prop("readonly",false);
$(".btn-submit").prop("disabled",false);
$(".bootbox-close-button").show();
buttonspinner.remove();
if (data.status) {
dialog.modal('hide');
$table.bootstrapTable('refresh');
}else{
alert(data.response);
}
},error: function(xhr, statusText, errorThrown) {
alert(xhr.responseText);
// animation
$("input", form).prop("readonly",false);
$(".btn-submit").prop("disabled",false);
$(".bootbox-close-button").show();
buttonspinner.remove();
}
});
});
}
</script>
<?= $this->endSection() ?>
Uji Coba Aplikasi
jalankan webserve menggunakan spark
php spark serve
buka browser dan kunjungi alamt http://localhost:8080
berikut hasilnya