Tutorial Membuat Multi User CodeIgniter 4

irfan-photo irfan · 2 bulan lalu

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

hasil praktek multi user codeigniter 4

Download Source Code

Terakhir diupdate 19 Jun 2022 06:33 WIB
Daftar Konten Belajar CodeIgniter 4