belajar dasar CodeIgniter 4 sudah dilakukan, sekarang saatnya untuk mengetes kemampuan dengan membuat proses CRUD
CRUD itu singkatan dari create, read, update, delete
jadi nantinya aplikasi CRUD ini akan menjadi sebuah aplikasi daftar-produk yang memiliki proses seperti diatas
untuk lebih jelasnya seperti apa CRUD yang akan dibuat berikut spesifikasinya
- Menggunakan Ajax
- Dengan Validasi Data (client dan server)
- Menghandle File Upload
- Menggunakan Plugin jQuery, Bootstrap 5, bootbox.js dan bootstrap-table (CDN)
- Membuat 2 Halaman yaitu homepage dan product
- CRUD Single Page
Persiapan
sebelum membuat aplikasi CRUD ini pastikan kalian sudah melakukan hal berikut
Pastikan kalian juga punya koneksi internet karena tutorial ini akan menggunakan script CDN yang dimana untuk mengaksesnya membutuhkan internet
Membuat Database dan Table
kita gunakan spark untuk membuat database dan tablenya menggunakan migration
Langkah 1 (Konfigurasi Database)
buka file '.env' dan edit bagian databasenya
#--------------------------------------------------------------------
# DATABASE
#--------------------------------------------------------------------
database.default.hostname = localhost
database.default.database = belajar_ci4
database.default.username = root
database.default.password =
database.default.DBDriver = MySQLi
database.default.DBPrefix =
Langkah 2 (Buat Database)
ketikan perintah spark
php spark db:create belajar_ci4
Langkah 3 (Buat Migration)
ketikan perintah spark
php spark make:migration product
buka file migration yang telah dibuat terletak di 'app/Database/Migrations/DATETIME_Product.php'
Ganti isi Kodenya dengan kode dibawah ini
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class Product extends Migration
{
public function up()
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'constraint' => 100,
'unsigned' => true,
'auto_increment' => true,
],
'name' => [
'type' => 'VARCHAR',
'constraint' => '150',
],
'category' => [
'type' => 'VARCHAR',
'constraint' => '100',
],
'price' => [
'type' => 'INT',
'default' => 0,
],
'photo' => [
'type' => 'VARCHAR',
'constraint' => '200',
],
'created_at' => [
'type' => 'DATETIME',
],
'updated_at' => [
'type' => 'DATETIME',
]
]);
$this->forge->addKey('id', true);
$this->forge->createTable('product');
}
public function down()
{
$this->forge->dropTable('product');
}
}
Langkah 4 (Jalankan Proses Migrasi)
ketikan perintah spark dibawah ini untuk mnejalankan migrasi yang telah dibuat
php spark migrate
Membuat Controller
kita gunakan spark untuk membuat controllernya dengan mengetikan perintah
php spark make:controller product
php spark make:controller homepage
dilanjut dengan mengedit controllernya
buka file controller homepage terletak di 'app/Controllers/Homepage.php'
edit kodenya menjadi seperti dibawah ini
<?php
namespace App\Controllers;
use App\Controllers\BaseController;
class Homepage extends BaseController
{
public function index()
{
$data['title'] = 'Homepage';
$productModel = new \App\Models\Product();
$data['products'] = $productModel->getData();
return view('homepage', $data);
}
}
edit kode controller product 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();
$productModel = new \App\Models\Product();
$productModel->insert($data);
return $this->response->setJSON([
'status' => true,
'response' => 'Success create data '.$data['name']
]);
}
public function edit()
{
$id = $this->request->getPost('hash');
$productModel = new \App\Models\Product();
$product = $productModel->select('name,category,price,photo')->find($id);
// build hash
$product['hash'] = $id;
return $this->response->setJSON([
'status' => true,
'response' => $product
]);
}
public function update()
{
$id = $this->request->getPost('hash');
$data = $this->_post_product($id);
$productModel = new \App\Models\Product();
$productModel->update($id, $data);
return $this->response->setJSON([
'status' => true,
'response' => 'Success update data '.$data['name']
]);
}
public function delete()
{
$id = $this->request->getPost('hash');
$productModel = new \App\Models\Product();
// read first
$product = $productModel->select('photo')->find($id);
// 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()
{
$ids = $this->request->getPost('ids');
$ids_array = explode("','", $ids);
$count = count($ids_array);
$productModel = new \App\Models\Product();
// read first
$products = $productModel->select('id,photo')->find($ids_array);
// 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 '
]);
}
}
Mengatur Route
buka file routes terletak di 'app/Config/Routes.php'
ganti default controllernya ke homepage
$routes->setDefaultController('Homepage');
ganti kode default Route Definitions dengan kode dibawah ini
// route homepage
$routes->get('/', 'homepage::index');
// route product
$routes->get('/product', 'product::index');
$routes->get('/product/read', 'product::read');
$routes->post('/product/create', 'product::create');
$routes->post('/product/edit', 'product::edit');
$routes->post('/product/update', 'product::update');
$routes->post('/product/delete', 'product::delete');
$routes->post('/product/delete_batch', 'product::delete_batch');
Membuat Model
ketikan perintah spark
php spark make:model product
edit model product dengan kode dibawah ini, file terletak di 'app/Models/Product.php'
<?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->orderBy('id','DESC')->findAll();
// load helper
helper('number');
// build data
$data = [];
foreach ($products as $product) {
$data[] = array(
'hash' => $product['id'],
'id' => $product['id'],
'name' => $product['name'],
'category' => $product['category'],
'price' => number_to_currency($product['price'], "IDR", "id", 0),
'photo' => base_url('uploads/'.$product['photo'])
);
}
return $data;
}
public function getDataForBootstrapTable($request)
{
$builder = $this->select('id, name, price');
// 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');
// build data
$data = [];
foreach ($products as $product) {
$data[] = array(
'hash' => $product['id'],
'id' => $product['id'],
'name' => $product['name'],
'price' => number_to_currency($product['price'], "IDR", "id", 0),
);
}
return [
'total' => $total,
'totalNotFiltered' => $total_filter,
'rows' => $data
];
}
}
Membuat View
silahkan buat folder dan file seperti struktur dibawah ini
.
└── app/
└── Views/
├── _layout/
│ ├── css.php
│ ├── footer.php
│ ├── js.php
│ └── nav.php
├── _layout.php
├── homepage.php
└── product.php
dilanjut dengan mengis kode disetiap file view yang telah dibuat
edit file view _layout.php dengan kode dibawah ini
<!DOCTYPE html>
<html class="h-100">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= (!empty($title) ? $title : 'No Title') ?></title>
<?= $this->include('_layout/css') ?>
</head>
<body class="d-flex flex-column h-100">
<?= $this->include('_layout/nav') ?>
<?= $this->renderSection('content') ?>
<?= $this->include('_layout/footer') ?>
<?= $this->include('_layout/js') ?>
</body>
</html>
edit file view homepage.php dengan kode 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-3 row-cols-md-4 g-3">
<?php foreach ($products as $product): ?>
<div class="col">
<div class="card shadow-sm">
<img class="bd-placeholder-img card-img-top" src="<?= $product['photo'] ?>">
<div class="card-body">
<h5 class="card-title">
<a class="text-decoration-none" href="#">
<?= $product['name'] ?>
</a>
</h5>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted"><?= $product['price'] ?></small>
<span class="badge bg-dark">
<?= $product['category'] ?>
</span>
</div>
</div>
</div>
</div>
<?php endforeach ?>
</div>
</div>
</main>
<?= $this->endSection() ?>
edit file view 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.identity, 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');
$.post(base_url + `/product/edit`, {hash : hash})
.done(function(data){
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.id
});
// validate
if (ids.length < 1) {return false;}
// convert
ids = ids.join("','");
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) {
$.post(base_url + `/product/delete_batch`, { ids:ids }, function(data) {}, 'json')
.done(function(data){
if (data.status) {
$table.bootstrapTable('refresh');
}
})
.fail(function(xhr, statusText, errorThrown) {
alert(xhr.responseText);
});
}
}
});
});
</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() ?>
edit file view _layout/css.php dengan kode dibawah ini
<!-- bootstrap -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.0-beta1/css/bootstrap.min.css" integrity="sha512-o/MhoRPVLExxZjCFVBsm17Pkztkzmh7Dp8k7/3JrtNCHh0AQ489kwpfA3dPSHzKDe8YCuEhxXq3Y71eb/o6amg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<!-- bootstrap-icon -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-icons/1.8.1/font/bootstrap-icons.min.css" integrity="sha512-Oy+sz5W86PK0ZIkawrG0iv7XwWhYecM3exvUtMKNJMekGFJtVAhibhRPTpmyTj8+lJCkmWfnpxKgT2OopquBHA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<?= $this->renderSection('css') ?>
edit file view _layout/footer.php dengan kode dibawah ini
<footer class="footer mt-auto py-3 bg-light">
<div class="container">
<span class="text-muted">© <?= date('Y') ?> Kurteyki - Belajar CRUD CI 4</span>
</div>
</footer>
edit file view _layout/js.php dengan kode dibawah ini
<script>
let base_url = '<?= base_url() ?>',
current_url = '<?= current_url() ?>';
// setting
xsetting = {
spinner : ` <div class="spinner-border spinner-border-sm button-spinner" role="status"></div>`
}
</script>
<!-- bootsrap -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.0-beta1/js/bootstrap.bundle.min.js" integrity="sha512-ndrrR94PW3ckaAvvWrAzRi5JWjF71/Pw7TlSo6judANOFCmz0d+0YE+qIGamRRSnVzSvIyGs4BTtyFMm3MT/cg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- jquery -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" integrity="sha512-894YE6QWD5I59HgZOGReFYm4dnWc1Qt5NtvYSaNcOP+u1T9qYdvdihz0PPSiiqn/+/3e7Jo4EaG7TubfWGUrMQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- bootbox -->
<style type="text/css">
/* bootbox clos-button fix */
.bootbox-close-button{box-sizing:content-box;color:#000!important;opacity:initial!important;border:0;border-radius:0.25rem;background:none;font-size:24px!important;font-weight:bold;padding:0;margin:0;float:initial!important}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/5.5.2/bootbox.min.js" integrity="sha512-RdSPYh1WA6BF0RhpisYJVYkOyTzK4HwofJ3Q7ivt/jkpW6Vc8AurL1R+4AUcvn9IwEKAPm/fk7qFZW3OuiUDeg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<?= $this->renderSection('js') ?>
edit file view _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>
</div>
</div>
</nav>
Membuat Folder Uploads
silahkan buat folder uploads didalam folder public, jadinya akan seperti ini
.
└── public/
├── uploads/
├── .htacces(s)
├── favicon.ico
├── index.php
└── robots.txt
Uji Coba Aplikasi
jalankan web server dengan spark
php spark serve
buka url http://localhost:8080 dibrowser kalian
dan berikut hasilnya