Kotchasan Framework Documentation

Kotchasan Framework Documentation

Practical Web Development Examples

EN 06 Feb 2026 07:31

Practical Web Development Examples

This guide presents real, working examples from the NowJS Bookstore project, demonstrating practical web application development with the Kotchasan Framework.

Note: All code examples in this document are taken directly from the actual project and are 100% tested and working.

Table of Contents

  1. Product Management System
  2. User Management System
  3. API Development with Gcms\Table
  4. Complete Working Examples

Product Management System

This section demonstrates a real product management system with CRUD operations, relations, and DataTable integration.

Database Structure

-- Products table
CREATE TABLE `product` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `module_id` int(11) NOT NULL,
    `product_no` varchar(50),
    `topic` varchar(255) NOT NULL,
    `isbn` varchar(50),
    `price` decimal(10,2) NOT NULL DEFAULT 0.00,
    `stock` int(11) NOT NULL DEFAULT 0,
    `page` int(11),
    `published` tinyint(1) DEFAULT 1,
    `recommend` tinyint(1) DEFAULT 0,
    `new` tinyint(1) DEFAULT 0,
    `hot` tinyint(1) DEFAULT 0,
    `create_date` datetime DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    KEY `published` (`published`),
    KEY `module_id` (`module_id`)
);

-- Product details (relations)
CREATE TABLE `product_details` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `product_id` int(11) NOT NULL,
    `title` varchar(255),
    `author_id` int(11),
    `category_id` int(11),
    PRIMARY KEY (`id`),
    KEY `product_id` (`product_id`),
    FOREIGN KEY (`product_id`) REFERENCES `product`(`id`) ON DELETE CASCADE
);

-- Product select options (categories, authors, etc.)
CREATE TABLE `product_select` (
    `select_id` int(11) NOT NULL AUTO_INCREMENT,
    `topic` varchar(255) NOT NULL,
    `type` varchar(50) NOT NULL,
    PRIMARY KEY (`select_id`),
    KEY `type` (`type`)
);

Product Model

This model handles CRUD operations for products with relations.

/**
 * Product Model
 * @filesource modules/product/models/product.php
 */

namespace Product\Product;

use Kotchasan\Model;

class Model extends \Kotchasan\Model
{
    /**
     * Get product data with relations
     *
     * @param int $id Product ID (0 for new product)
     * @return array Product data with options and relations
     */
    public static function get($id)
    {
        $product = [];

        if ($id === 0) {
            // New Product - return empty structure
            $product['data'] = (object) [
                'id' => 0,
                'topic' => '',
                'stock' => 0,
                'price' => 0,
                'published' => 1,
                'author_id' => [],
                'category_id' => []
            ];
            $product['options'] = self::getOptions();
            $product['relations'] = [];

            return $product;
        }

        // Fetch existing product
        $product['data'] = static::createQuery()
            ->select()
            ->from('product')
            ->where([['id', $id]])
            ->first();

        if ($product['data']) {
            $product['options'] = self::getOptions();

            // Load product relations (authors, categories, etc.)
            $product['relations']['data'] = static::createQuery()
                ->select('id', 'title', 'author_id', 'category_id')
                ->from('product_details')
                ->where([['product_id', $product['data']->id]])
                ->execute()
                ->fetchAll();
        }

        return $product;
    }

    /**
     * Get select options for product form
     * Returns authors, categories, etc.
     *
     * @return array Options grouped by type
     */
    public static function getOptions()
    {
        $options = [];

        $result = static::createQuery()
            ->select('select_id', 'topic', 'type')
            ->from('product_select')
            ->orderBy('topic')
            ->execute();

        foreach ($result->fetchAll() as $row) {
            $options[$row->type][] = [
                'value' => $row->select_id,
                'text' => $row->topic
            ];
        }

        return $options;
    }

    /**
     * Save product data with relations
     *
     * @param \Kotchasan\DB $db Database connection
     * @param int $id Product ID (0 for new)
     * @param array $save Product data
     * @param array $relations Related data (authors, categories)
     * @return int Product ID
     */
    public static function save($db, $id, $save, $relations)
    {
        // 1. Save main product data
        if ($id > 0) {
            // Update existing product
            $db->update('product', [['id', $id]], $save);
        } else {
            // Insert new product
            $id = $db->insert('product', $save);
        }

        // 2. Delete all existing relations
        $db->delete('product_details', [['product_id', $id]], 0);

        // 3. Insert new relations
        foreach ($relations['title'] as $key => $value) {
            if ($value !== '') {
                $db->insert('product_details', [
                    'product_id' => $id,
                    'author_id' => $relations['author_id'][$key],
                    'category_id' => $relations['category_id'][$key],
                    'title' => $value
                ]);
            }
        }

        return $id;
    }
}

Products Model (DataTable)

This model provides data for the products listing table.

/**
 * Products Model
 * @filesource modules/product/models/products.php
 */

namespace Product\Products;

use Kotchasan\Model;

class Model extends \Kotchasan\Model
{
    /**
     * Query products for DataTable
     * Supports search and filtering
     *
     * @param array $params Query parameters (search, published, etc.)
     * @return \Kotchasan\QueryBuilder\QueryBuilderInterface
     */
    public static function toDataTable($params)
    {
        $where = [];

        // Filter by published status
        if ($params['published'] !== '') {
            $where[] = ['published', (int) $params['published']];
        }

        $query = static::createQuery()
            ->select(
                'id',
                'module_id',
                'product_no',
                'topic',
                'isbn',
                'price',
                'stock',
                'published',
                'create_date'
            )
            ->from('product')
            ->where($where);

        // Search across multiple fields (OR condition)
        if (!empty($params['search'])) {
            $search = '%' . $params['search'] . '%';
            $query->where([
                ['topic', 'LIKE', $search],
                ['isbn', 'LIKE', $search],
                ['product_no', 'LIKE', $search]
            ], 'OR');
        }

        return $query;
    }

    /**
     * Delete products by IDs
     * Also deletes related product_details
     *
     * @param int|array $ids Product ID or array of IDs
     * @return int Number of deleted products
     */
    public static function remove($ids)
    {
        if (empty($ids)) {
            return 0;
        }

        // 1. Delete product details (relations)
        static::createQuery()
            ->delete('product_details')
            ->where([['product_id', $ids]])
            ->execute();

        // 2. Delete products
        static::createQuery()
            ->delete('product')
            ->where([['id', $ids]])
            ->execute();

        // 3. Delete product images (if needed)
        // foreach ((array) $ids as $id) {
        //     $img = ROOT_PATH.DATA_FOLDER.'product/'.$id.'.jpg';
        //     if (file_exists($img)) {
        //         unlink($img);
        //     }
        // }

        return count((array) $ids);
    }

    /**
     * Update product status fields
     * Used for bulk status updates (published, recommend, hot, new)
     *
     * @param int|array $ids Product IDs
     * @param string $column Column to update
     * @param int $value New value (0 or 1)
     * @return void
     */
    public static function updateStatus($ids, $column, $value)
    {
        if (empty($ids)) {
            return;
        }

        static::createQuery()
            ->update('product')
            ->set([$column => $value])
            ->where([['id', $ids]])
            ->execute();
    }
}

Product List Model (Public Display)

This model is used for displaying products on the public website.

/**
 * Product List Model
 * @filesource modules/product/models/list.php
 */

namespace Product\List;

use Kotchasan\KBase;
use Kotchasan\Model;

class Model extends \Kotchasan\KBase
{
    private $instance = null;

    /**
     * Create product list query with filters
     *
     * @param array $params Query parameters
     * @return self
     */
    public static function create($params)
    {
        $obj = new static();

        // Base conditions
        $where = [
            ['P.published', 1]  // Only published products
        ];

        // Optional filters
        if (!empty($params['recommend'])) {
            $where[] = ['P.recommend', 1];
        }
        if (!empty($params['new'])) {
            $where[] = ['P.new', 1];
        }
        if (!empty($params['hot'])) {
            $where[] = ['P.hot', 1];
        }

        // Build base query
        $obj->instance = Model::createQuery()
            ->from('product P')
            ->where($where)
            ->cacheOn();  // Enable query caching

        // Filter by category (with EXISTS subquery)
        if (!empty($params['category_id'])) {
            $category_ids = explode(',', $params['category_id']);

            $obj->instance->whereExists(
                ['product_details D', 'product_select S'],
                [
                    ['D.product_id', 'P.id'],
                    ['D.category_id', $category_ids],
                    ['S.type', 'category_id'],
                    ['S.select_id', $category_ids]
                ]
            );
        }

        return $obj;
    }

    /**
     * Execute query and return products
     *
     * @param int $limit Number of products to return
     * @return array Products list
     */
    public function execute($limit)
    {
        return $this->instance
            ->select(
                'P.id',
                'P.module_id',
                'P.product_no',
                'P.topic',
                'P.isbn',
                'P.price',
                'P.stock',
                'P.page',
                'P.recommend',
                'P.new',
                'P.hot'
            )
            ->where(['P.stock', '>', 0])  // Only in-stock products
            ->orderBy('create_date', 'DESC')
            ->limit($limit)
            ->fetchAll();
    }
}

Usage Example:

// Get recommended new products
$products = \Product\List\Model::create([
    'recommend' => 1,
    'new' => 1,
    'category_id' => '5,8,12'
])->execute(10);

foreach ($products as $product) {
    echo $product->topic . ' - $' . $product->price . PHP_EOL;
}

Products Controller (API Endpoint)

This controller extends Gcms\Table to provide a complete API endpoint for product management.

/**
 * Products Controller
 * @filesource modules/product/controllers/products.php
 */

namespace Product\Products;

use Gcms\Api as ApiController;
use Kotchasan\Http\Request;

class Controller extends \Gcms\Table
{
    /**
     * Allowed sort columns (SQL injection prevention)
     */
    protected $allowedSortColumns = [
        'id',
        'product_no',
        'topic',
        'isbn',
        'price',
        'stock',
        'published',
        'create_date'
    ];

    /**
     * Get custom parameters for table filtering
     *
     * @param Request $request
     * @param object $login Current user
     * @return array
     */
    protected function getCustomParams(Request $request, $login): array
    {
        return [
            'published' => $request->get('published')->filter('01')
        ];
    }

    /**
     * Check authorization
     * Only users with product permissions can access
     *
     * @param Request $request
     * @param object $login
     * @return true|array
     */
    protected function checkAuthorization(Request $request, $login)
    {
        if (!ApiController::hasPermission($login, ['can_manage_product', 'can_view_product'])) {
            return $this->errorResponse('Permission required', 403);
        }

        return true;
    }

    /**
     * Query data for DataTable
     *
     * @param array $params
     * @param object $login
     * @return \Kotchasan\QueryBuilder\QueryBuilderInterface
     */
    protected function toDataTable($params, $login = null)
    {
        return \Product\Products\Model::toDataTable($params);
    }

    /**
     * Format data list with additional display fields
     *
     * @param array $datas
     * @param object $login
     * @return array
     */
    protected function formatDatas(array $datas, $login = null): array
    {
        $data = [];

        foreach ($datas as $row) {
            // Add product thumbnail
            $imgPath = ROOT_PATH . DATA_FOLDER . 'product/' . $row->module_id . '-' . $row->id . '.jpg';

            if (file_exists($imgPath)) {
                $row->thumb = WEB_URL . DATA_FOLDER . 'product/' . $row->module_id . '-' . $row->id . '.jpg';
            } else {
                $row->thumb = WEB_URL . 'images/no-image.webp';
            }

            $data[] = $row;
        }

        return $data;
    }

    /**
     * Get filters for table response
     *
     * @param array $params
     * @param object $login
     * @return array
     */
    protected function getFilters($params, $login = null)
    {
        return [
            'published' => self::getPublishedOptions()
        ];
    }

    /**
     * Handle edit action
     * Redirects to product edit page
     *
     * @param Request $request
     * @param object $login
     * @return array
     */
    protected function handleEditAction(Request $request, $login)
    {
        if (!ApiController::hasPermission($login, ['can_manage_product'])) {
           return $this->errorResponse('Failed to process request', 403);
        }

        $id = $request->post('id')->toInt();

        return $this->redirectResponse('/product?id=' . $id);
    }

    /**
     * Handle delete action
     * Deletes selected products
     *
     * @param Request $request
     * @param object $login
     * @return array
     */
    protected function handleDeleteAction(Request $request, $login)
    {
        if (!ApiController::canModify($login, ['can_manage_product'])) {
            return $this->errorResponse('Failed to process request', 403);
        }

        $ids = $request->request('ids', [])->toInt();
        $removeCount = \Product\Products\Model::remove($ids);

        if (empty($removeCount)) {
            return $this->errorResponse('Delete action failed', 400);
        }

        // Log the action
        \Index\Log\Model::add(
            0,
            'Product',
            'Delete Product ID(s) : ' . implode(', ', $ids),
            $login->id
        );

        return $this->redirectResponse('reload', 'Deleted ' . $removeCount . ' product(s) successfully');
    }

    /**
     * Handle status action (published|1, published|0, etc.)
     *
     * @param Request $request
     * @param object $login
     * @return array
     */
    protected function handleStatusAction(Request $request, $login)
    {
        if (!ApiController::canModify($login, ['can_manage_product'])) {
            return $this->errorResponse('Failed to process request', 403);
        }

        $ids = $request->request('ids', [])->toInt();
        $status = $request->request('status')->filter('a-z0-9_');

        // Parse status: published_1, recommend_0, etc.
        if (preg_match('/^([a-z]+)_([0-1])$/', $status, $match)) {
            $column = $match[1];
            $value = (int) $match[2];

            // Validate column
            if (in_array($column, ['published', 'recommend', 'hot', 'new'])) {
                \Product\Products\Model::updateStatus($ids, $column, $value);

                // Log the action
                \Index\Log\Model::add(
                    0,
                    'Product',
                    'Update Product ID(s) : ' . implode(', ', $ids) . ' ' . $column . '=' . $value,
                    $login->id
                );

                return $this->redirectResponse('reload', 'Updated successfully');
            }
        }

        return $this->errorResponse('Invalid status action', 400);
    }

    /**
     * Get published status options
     *
     * @return array
     */
    public static function getPublishedOptions()
    {
        return [
            ['value' => '1', 'text' => 'Active'],
            ['value' => '0', 'text' => 'Inactive']
        ];
    }
}

API Endpoints:

GET  /api/products              - List products with pagination
POST /api/products/action       - Bulk actions (edit, delete, status)
GET  /api/products/export       - Export products to CSV

User Management System

This section demonstrates real user management with role-based permissions, status handling, and bulk actions.

Database Structure

-- Users table
CREATE TABLE `user` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `username` varchar(255) NOT NULL,
    `password` varchar(255) NOT NULL,
    `name` varchar(255) NOT NULL,
    `phone` varchar(20),
    `email` varchar(255),
    `status` tinyint(1) NOT NULL DEFAULT 0,
    `active` tinyint(1) NOT NULL DEFAULT 1,
    `social` tinyint(1) NOT NULL DEFAULT 0,
    `activatecode` varchar(32),
    `create_date` datetime DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `username` (`username`),
    KEY `status` (`status`),
    KEY `active` (`active`)
);

Users Model

/**
 * Users Model
 * @filesource modules/index/models/users.php
 */

namespace Index\Users;

use Kotchasan\Model;

class Model extends \Kotchasan\Model
{
    /**
     * Social icon mapping
     */
    public static $socialIcons = [
        1 => 'icon-facebook',
        2 => 'icon-google',
        3 => 'icon-line',
        4 => 'icon-telegram'
    ];

    /**
     * Get social icon class
     *
     * @param int $social Social login type
     * @return string Icon class name
     */
    public static function getSocialIcon($social)
    {
        return self::$socialIcons[$social] ?? 'icon-user';
    }

    /**
     * Query users for DataTable
     * Supports status filtering and search
     *
     * @param array $params Query parameters
     * @return \Kotchasan\QueryBuilder\QueryBuilderInterface
     */
    public static function toDataTable($params)
    {
        // Filters (AND conditions)
        $where = [];

        if (isset($params['status']) && $params['status'] !== '') {
            $where[] = ['U.status', (int) $params['status']];
        }

        // Build base query
        $query = static::createQuery()
            ->select(
                'U.id',
                'U.username',
                'U.name',
                'U.phone',
                'U.status',
                'U.active',
                'U.social',
                'U.create_date'
            )
            ->from('user U')
            ->where($where);

        // Search (OR condition)
        if (!empty($params['search'])) {
            $search = '%' . $params['search'] . '%';
            $where = [
                ['U.name', 'LIKE', $search],
                ['U.username', 'LIKE', $search],
                ['U.phone', 'LIKE', $search]
            ];

            $query->where($where, 'OR');
        }

        return $query;
    }

    /**
     * Delete user(s) by ID
     * Also deletes user avatar files
     *
     * @param int|array $ids User ID or array of IDs
     * @return int Number of deleted users
     */
    public static function remove($ids)
    {
        $remove_ids = [];

        // Delete files for each user
        foreach ((array) $ids as $id) {
            // Protect admin user (ID=1)
            if ($id == 1) {
                continue;
            }

            // Delete user avatar image
            $img = ROOT_PATH . DATA_FOLDER . 'avatar/' . $id . self::$cfg->stored_img_type;
            if (file_exists($img)) {
                unlink($img);
            }

            $remove_ids[] = $id;
        }

        if (empty($remove_ids)) {
            return 0;
        }

        // Delete users from database
        return \Kotchasan\DB::create()
            ->delete('user', [['id', $remove_ids]]);
    }

    /**
     * Convert users to select options
     * Used for dropdowns, multi-select, etc.
     *
     * @param array $where Optional where conditions
     * @return array Array of ['value', 'text'] pairs
     */
    public static function toOptions($where = [])
    {
        return static::createQuery()
            ->select('id value', 'name text')
            ->from('user')
            ->where($where)
            ->orderBy('name')
            ->execute()
            ->fetchAll();
    }
}

Users Controller

/**
 * Users Controller
 * @filesource modules/index/controllers/users.php
 */

namespace Index\Users;

use Gcms\Api as ApiController;
use Kotchasan\Http\Request;
use Kotchasan\Http\Response;

class Controller extends \Gcms\Table
{
    /**
     * Social login types
     */
    public static $socialTypes = [
        0 => 'Registered',
        1 => 'Facebook',
        2 => 'Google',
        3 => 'LINE',
        4 => 'Telegram'
    ];

    /**
     * Allowed sort columns
     */
    protected $allowedSortColumns = ['id', 'name', 'active', 'create_date', 'status'];

    /**
     * Get custom parameters
     *
     * @param Request $request
     * @param object $login
     * @return array
     */
    protected function getCustomParams(Request $request, $login): array
    {
        return [
            'status' => $request->get('status')->number()
        ];
    }

    /**
     * Check authorization
     * Only super admins can manage users
     *
     * @param Request $request
     * @param object $login
     * @return true|array
     */
    protected function checkAuthorization(Request $request, $login)
    {
        if (!ApiController::isSuperAdmin($login)) {
            return $this->errorResponse('Permission required', 403);
        }

        return true;
    }

    /**
     * Query data for DataTable
     *
     * @param array $params
     * @param object $login
     * @return \Kotchasan\QueryBuilder\QueryBuilderInterface
     */
    protected function toDataTable($params, $login = null)
    {
        return \Index\Users\Model::toDataTable($params);
    }

    /**
     * Format user list with additional display fields
     *
     * @param array $datas
     * @param object $login
     * @return array
     */
    protected function formatDatas(array $datas, $login = null): array
    {
        $data = [];

        foreach ($datas as $row) {
            // Add status text
            $row->status_text = self::$cfg->member_status[$row->status] ?? $row->status;

            // Add user initials
            $row->initial_name = self::getInitialName($row->name);

            // Add avatar URL
            $avatarPath = ROOT_PATH . DATA_FOLDER . 'avatar/' . $row->id . self::$cfg->stored_img_type;

            if (file_exists($avatarPath)) {
                $row->avatar = WEB_URL . DATA_FOLDER . 'avatar/' . $row->id . self::$cfg->stored_img_type;
            } else {
                $row->avatar = null;
            }

            $data[] = $row;
        }

        return $data;
    }

    /**
     * Get filters for table
     *
     * @param array $params
     * @param object $login
     * @return array
     */
    protected function getFilters($params, $login = null)
    {
        return [
            'status' => \Gcms\Controller::getUserStatusOptions()
        ];
    }

    /**
     * Handle delete action
     *
     * @param Request $request
     * @param object $login
     * @return array
     */
    protected function handleDeleteAction(Request $request, $login)
    {
        if (!ApiController::isSuperAdmin($login)) {
            return $this->errorResponse('Failed to process request', 403);
        }

        $ids = $request->request('ids', [])->toInt();
        $removeCount = \Index\Users\Model::remove($ids);

        if (empty($removeCount)) {
            return $this->errorResponse('Delete action failed', 400);
        }

        \Index\Log\Model::add(
            0,
            'Index',
            'Delete User ID(s) : ' . implode(', ', $ids),
            $login->id
        );

        return $this->redirectResponse('reload', 'Deleted ' . $removeCount . ' user(s) successfully');
    }

    /**
     * Handle edit action
     * Redirects to profile edit page
     *
     * @param Request $request
     * @param object $login
     * @return array
     */
    protected function handleEditAction(Request $request, $login)
    {
        $id = $request->post('id')->toInt();

        // Users can edit themselves, admins can edit anyone
        if (!ApiController::isSuperAdmin($login) && $id !== $login->id) {
            return $this->errorResponse('Failed to process request', 403);
        }

        return $this->redirectResponse('/profile?id=' . $id);
    }

    /**
     * Handle activate action
     * Accept member verification request
     *
     * @param Request $request
     * @param object $login
     * @return Response
     */
    protected function handleActivateAction(Request $request, $login)
    {
        if (!ApiController::isAdmin($login)) {
            return $this->errorResponse('Failed to process request', 403);
        }

        $ids = $request->post('ids', [])->toInt();

        if (empty($ids)) {
            return $this->errorResponse('No users selected', 400);
        }

        // Update users - set active and clear activation code
        $editCount = \Kotchasan\DB::create()
            ->update('user', [
                ['id', $ids],
                ['id', '!=', 1]  // Protect admin user
            ], [
                'active' => 1,
                'activatecode' => ''
            ]);

        // Log the action
        \Index\Log\Model::add(
            0,
            'Index',
            'Accept verification: ' . implode(',', $ids),
            $login->id
        );

        return $this->redirectResponse('reload', 'Accept verification ' . $editCount . ' user(s)');
    }

    /**
     * Get initial name from full name
     * Returns first 2 letters of name or first letters of first and last name
     *
     * @param string $name Full name
     * @return string Initial name (2 characters)
     */
    protected static function getInitialName($name)
    {
        // Try to get first letter of first and last name
        if (preg_match_all('/([a-zA-Zก-ฮ]{1}).*?\s.*?([a-zA-Zก-ฮ]{1})/u', trim($name), $matches)) {
            return $matches[1][0] . $matches[2][0];
        }

        // Fallback: return first 2 characters
        return mb_substr($name, 0, 2, 'utf-8');
    }
}

API Endpoints:

GET  /api/users                 - List users with pagination
POST /api/users/action          - Bulk actions (edit, delete, activate, etc.)

API Development with Gcms\Table

The Gcms\Table base class provides a complete API framework for table-based data management. This section explains how to use it effectively.

Gcms\Table Overview

Gcms\Table is a powerful base controller that provides:

  • Automatic pagination and sorting
  • Dynamic action handling via method naming convention
  • Permission checking hooks
  • CSV/PDF export support
  • DataTable integration
  • SQL injection prevention

Basic Usage Pattern


namespace MyModule\MyResource;

use Gcms\Api as ApiController;
use Kotchasan\Http\Request;

class Controller extends \Gcms\Table
{
    // 1. Define sortable columns (SQL injection prevention)
    protected $allowedSortColumns = ['id', 'name', 'status', 'created_at'];

    // 2. Override authorization check
    protected function checkAuthorization(Request $request, $login)
    {
        if (!ApiController::hasPermission($login, ['can_manage_resource'])) {
            return $this->errorResponse('Permission required', 403);
        }
        return true;
    }

    // 3. Implement data query
    protected function toDataTable($params, $login = null)
    {
        return \MyModule\MyResource\Model::toDataTable($params);
    }

    // 4. Add custom parameters
    protected function getCustomParams(Request $request, $login): array
    {
        return [
            'status' => $request->get('status')->filter('01'),
            'category' => $request->get('category')->toInt()
        ];
    }

    // 5. Format output data
    protected function formatDatas(array $datas, $login = null): array
    {
        foreach ($datas as $row) {
            $row->formatted_date = date('M d, Y', strtotime($row->created_at));
            $row->status_label = $row->status ? 'Active' : 'Inactive';
        }
        return $datas;
    }

    // 6. Add filters
    protected function getFilters($params, $login = null)
    {
        return [
            'status' => [
                ['value' => '1', 'text' => 'Active'],
                ['value' => '0', 'text' => 'Inactive']
            ]
        ];
    }
}

Action Handlers

Actions are handled via method naming convention: handle{Action}Action

/**
 * Handle delete action
 * Triggered by: POST /api/resource/action?action=delete
 */
protected function handleDeleteAction(Request $request, $login)
{
    // Check permission
    if (!ApiController::canModify($login, ['can_delete_resource'])) {
        return $this->errorResponse('Permission denied', 403);
    }

    // Get selected IDs
    $ids = $request->request('ids', [])->toInt();

    // Perform deletion
    $count = \MyModule\MyResource\Model::remove($ids);

    // Log the action
    \Index\Log\Model::add(0, 'MyModule', 'Deleted resources: ' . implode(', ', $ids), $login->id);

    // Return success response with reload instruction
    return $this->redirectResponse('reload', 'Deleted ' . $count . ' item(s)');
}

/**
 * Handle custom action: send_notification
 * Triggered by: POST /api/resource/action?action=send_notification
 */
protected function handleSendNotificationAction(Request $request, $login)
{
    $ids = $request->request('ids', [])->toInt();
    $message = $request->request('message')->topic();

    $sentCount = 0;
    foreach ($ids as $id) {
        if (\MyModule\Notification\Model::send($id, $message)) {
            $sentCount++;
        }
    }

    return $this->redirectResponse('reload', 'Sent to ' . $sentCount . ' recipient(s)');
}

Export Handlers

Export handlers use the same naming convention: handle{Type}Export

/**
 * Handle CSV export
 * Triggered by: GET /api/resource/export?type=csv
 */
protected function handleCsvExport(Request $request, $login)
{
    $params = $this->parseParams($request, $login);

    // Define CSV headers
    $headers = ['ID', 'Name', 'Status', 'Created'];

    // Define row formatter
    $rowFormatter = function($row) {
        return [
            $row->id,
            $row->name,
            $row->status ? 'Active' : 'Inactive',
            date('Y-m-d', strtotime($row->created_at))
        ];
    };

    // Export to CSV (sends file and exits)
    $this->exportToCsv($params, $login, $headers, $rowFormatter, 'resources');
}

Permission Checking Patterns

// Check if user has specific permission(s)
if (ApiController::hasPermission($login, ['permission_name'])) {
    // User has permission
}

// Check if user can modify (not in demo mode)
if (ApiController::canModify($login, ['permission_name'])) {
    // User can modify
}

// Check if user is admin
if (ApiController::isAdmin($login)) {
    // User is admin
}

// Check if user is super admin
if (ApiController::isSuperAdmin($login)) {
    // User is super admin
}

Response Methods

// Success response (HTTP 200)
return $this->successResponse($data, 'Operation successful');

// Error response
return $this->errorResponse('Error message', 400);

// Redirect response (instructs client to reload or redirect)
return $this->redirectResponse('reload', 'Success message');
return $this->redirectResponse('/path/to/page', 'Redirecting...');

Complete Working Examples

This section provides complete, end-to-end examples showing how all pieces work together.

Example 1: Product Management Flow

Complete flow from listing to editing with permissions.

// 1. List products (GET /api/products?page=1&pageSize=25&published=1)
{
    "data": [
        {
            "id": 1,
            "topic": "Introduction to PHP",
            "isbn": "978-1234567890",
            "price": "29.99",
            "stock": 10,
            "published": 1,
            "thumb": "/datas/product/1-1.jpg"
        }
    ],
    "meta": {
        "page": 1,
        "pageSize": 25,
        "total": 100,
        "totalPages": 4
    },
    "filters": {
        "published": [
            {"value": "1", "text": "Active"},
            {"value": "0", "text": "Inactive"}
        ]
    }
}

// 2. Edit product (POST /api/products/action with action=edit&ids[]=1)
// Redirects to: /product?id=1

// 3. Save product
$product = \Product\Product\Model::get(1);

$save = [
    'topic' => 'Updated Product Title',
    'price' => 34.99,
    'stock' => 15,
    'published' => 1
];

$relations = [
    'title' => ['Related Item 1', 'Related Item 2'],
    'author_id' => [5, 8],
    'category_id' => [2, 3]
];

$db = \Kotchasan\DB::create();
$productId = \Product\Product\Model::save($db, 1, $save, $relations);

// 4. Update status (POST /api/products/action with action=status&status=published_0&ids[]=1,2,3)
// Sets published=0 for products 1, 2, 3

// 5. Delete products (POST /api/products/action with action=delete&ids[]=5,6,7)
// Deletes products 5, 6, 7 and their relations

Example 2: User Registration and Activation Flow

Complete user lifecycle from registration to activation.

// 1. User registration
$userData = [
    'username' => 'john.doe@example.com',
    'password' => password_hash('SecurePass123', PASSWORD_DEFAULT),
    'name' => 'John Doe',
    'phone' => '123-456-7890',
    'status' => 0,  // Pending
    'active' => 0,  // Not activated
    'activatecode' => md5(uniqid() . time())
];

$userId = \Kotchasan\DB::create()->insert('user', $userData);

// 2. Send activation email
\Index\Email\Model::sendActivation(
    $userData['username'],
    WEB_URL . 'activate?id=' . $userData['activatecode'],
    $userData['name']
);

// 3. Admin views pending users (GET /api/users?status=0)
$params = ['status' => 0, 'search' => '', 'page' => 1, 'pageSize' => 25];
$query = \Index\Users\Model::toDataTable($params);
$users = $query->execute()->fetchAll();

// 4. Admin activates users (POST /api/users/action with action=activate&ids[]=15,16,17)
\Kotchasan\DB::create()->update('user', [
    ['id', [15, 16, 17]],
    ['id', '!=', 1]
], [
    'active' => 1,
    'activatecode' => ''
]);

// 5. User can now login
$user = \Kotchasan\DB::create()->first('user', [
    ['username', 'john.doe@example.com'],
    ['active', 1]
]);

Example 3: Building a Custom API Controller

Complete example of creating a custom resource management API.

/**
 * Orders Controller
 * @filesource modules/shop/controllers/orders.php
 */

namespace Shop\Orders;

use Gcms\Api as ApiController;
use Kotchasan\Http\Request;

class Controller extends \Gcms\Table
{
    protected $allowedSortColumns = ['id', 'order_number', 'status', 'total', 'created_at'];

    protected function checkAuthorization(Request $request, $login)
    {
        if (!ApiController::hasPermission($login, ['can_view_orders'])) {
            return $this->errorResponse('Permission required', 403);
        }
        return true;
    }

    protected function getCustomParams(Request $request, $login): array
    {
        return [
            'status' => $request->get('status')->filter('a-z'),
            'start_date' => $request->get('start_date')->date(),
            'end_date' => $request->get('end_date')->date()
        ];
    }

    protected function toDataTable($params, $login = null)
    {
        $where = [];

        if (!empty($params['status'])) {
            $where[] = ['status', $params['status']];
        }

        if (!empty($params['start_date'])) {
            $where[] = ['created_at', '>=', $params['start_date'] . ' 00:00:00'];
        }

        if (!empty($params['end_date'])) {
            $where[] = ['created_at', '<=', $params['end_date'] . ' 23:59:59'];
        }

        $query = \Kotchasan\Model::createQuery()
            ->select('O.*', 'U.name customer_name')
            ->from('orders O')
            ->leftJoin('user U', 'U.id', 'O.user_id')
            ->where($where);

        if (!empty($params['search'])) {
            $search = '%' . $params['search'] . '%';
            $query->where([
                ['O.order_number', 'LIKE', $search],
                ['U.name', 'LIKE', $search]
            ], 'OR');
        }

        return $query;
    }

    protected function formatDatas(array $datas, $login = null): array
    {
        foreach ($datas as $row) {
            $row->formatted_total = '$' . number_format($row->total, 2);
            $row->formatted_date = date('M d, Y H:i', strtotime($row->created_at));
            $row->status_badge = $this->getStatusBadge($row->status);
        }
        return $datas;
    }

    protected function getFilters($params, $login = null)
    {
        return [
            'status' => [
                ['value' => 'pending', 'text' => 'Pending'],
                ['value' => 'processing', 'text' => 'Processing'],
                ['value' => 'shipped', 'text' => 'Shipped'],
                ['value' => 'delivered', 'text' => 'Delivered'],
                ['value' => 'cancelled', 'text' => 'Cancelled']
            ]
        ];
    }

    protected function handleViewAction(Request $request, $login)
    {
        $id = $request->post('id')->toInt();
        return $this->redirectResponse('/order/view?id=' . $id);
    }

    protected function handleShipAction(Request $request, $login)
    {
        if (!ApiController::canModify($login, ['can_manage_orders'])) {
            return $this->errorResponse('Permission denied', 403);
        }

        $ids = $request->request('ids', [])->toInt();

        \Kotchasan\DB::create()->update('orders', [
            ['id', $ids],
            ['status', 'processing']
        ], [
            'status' => 'shipped',
            'shipped_at' => date('Y-m-d H:i:s')
        ]);

        return $this->redirectResponse('reload', 'Orders marked as shipped');
    }

    protected function handleCsvExport(Request $request, $login)
    {
        $params = $this->parseParams($request, $login);

        $headers = ['Order ID', 'Order Number', 'Customer', 'Status', 'Total', 'Date'];

        $rowFormatter = function($row) {
            return [
                $row->id,
                $row->order_number,
                $row->customer_name,
                ucfirst($row->status),
                '$' . number_format($row->total, 2),
                date('Y-m-d H:i', strtotime($row->created_at))
            ];
        };

        $this->exportToCsv($params, $login, $headers, $rowFormatter, 'orders_export');
    }

    private function getStatusBadge($status)
    {
        $badges = [
            'pending' => '<span class="badge badge-warning">Pending</span>',
            'processing' => '<span class="badge badge-info">Processing</span>',
            'shipped' => '<span class="badge badge-primary">Shipped</span>',
            'delivered' => '<span class="badge badge-success">Delivered</span>',
            'cancelled' => '<span class="badge badge-danger">Cancelled</span>'
        ];

        return $badges[$status] ?? $status;
    }
}

Frontend JavaScript Integration:

// Fetch orders list
async function loadOrders(page = 1, filters = {}) {
    const params = new URLSearchParams({
        page: page,
        pageSize: 25,
        ...filters
    });

    const response = await fetch(`/api/orders?${params}`, {
        headers: {
            'X-CSRF-TOKEN': getCsrfToken()
        }
    });

    const result = await response.json();
    renderOrdersTable(result.data);
    renderPagination(result.meta);
}

// Bulk action: Ship orders
async function shipOrders(orderIds) {
    const response = await fetch('/api/orders/action', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'X-CSRF-TOKEN': getCsrfToken()
        },
        body: new URLSearchParams({
            action: 'ship',
            'ids[]': orderIds
        })
    });

    const result = await response.json();

    if (result.alert) {
        showNotification(result.alert);
    }

    if (result.location === 'reload') {
        loadOrders();
    }
}

// Export to CSV
function exportOrders(filters = {}) {
    const params = new URLSearchParams({
        type: 'csv',
        ...filters
    });

    window.location.href = `/api/orders/export?${params}`;
}

Best Practices

1. Always Use Prepared Statements

// ✅ GOOD: Using QueryBuilder (automatically uses prepared statements)
$products = \Kotchasan\Model::createQuery()
    ->from('product')
    ->where([['topic', 'LIKE', '%' . $search . '%']])
    ->execute();

// ❌ BAD: Direct SQL with user input
$products = \Kotchasan\DB::create()->customQuery(
    "SELECT * FROM product WHERE topic LIKE '%" . $_GET['search'] . "%'"
);

2. Validate User Input

// ✅ GOOD: Using Input filters
$id = $request->get('id')->toInt();  // Ensures integer
$email = $request->post('email')->email();  // Validates email format
$status = $request->get('status')->filter('01');  // Only allows '0' or '1'

// ❌ BAD: Using raw input
$id = $_GET['id'];
$email = $_POST['email'];

3. Check Permissions

// ✅ GOOD: Always check permissions before sensitive operations
if (!ApiController::canModify($login, ['can_delete_product'])) {
    return $this->errorResponse('Permission denied', 403);
}

// ❌ BAD: No permission check
$ids = $request->request('ids')->toInt();
\Product\Products\Model::remove($ids);

4. Log Important Actions

// ✅ GOOD: Log destructive operations
\Index\Log\Model::add(
    0,
    'Product',
    'Delete Product ID(s) : ' . implode(', ', $ids),
    $login->id
);

// Log user actions for audit trail
\Index\Log\Model::add(
    $userid,
    'Auth',
    'User login successful',
    $userid
);

5. Handle Errors Gracefully

// ✅ GOOD: Try-catch with meaningful error messages
try {
    $result = \Product\Product\Model::save($db, $id, $data, $relations);
    return $this->successResponse(['id' => $result], 'Product saved successfully');
} catch (\Exception $e) {
    // Log the error
    error_log('Product save failed: ' . $e->getMessage());

    // Return user-friendly message
    return $this->errorResponse('Failed to save product. Please try again.', 500);
}
// ✅ GOOD: Use transactions for multi-table operations
$db = \Kotchasan\DB::create();
$db->beginTransaction();

try {
    // Insert order
    $orderId = $db->insert('orders', $orderData);

    // Insert order items
    foreach ($items as $item) {
        $item['order_id'] = $orderId;
        $db->insert('order_items', $item);
    }

    // Update product stock
    foreach ($items as $item) {
        $db->createQuery()
            ->update('product')
            ->set(['stock' => 'stock - ' . $item['quantity']])
            ->where([['id', $item['product_id']]])
            ->execute();
    }

    $db->commit();
} catch (\Exception $e) {
    $db->rollback();
    throw $e;
}

7. Optimize Queries

// ✅ GOOD: Select only needed columns
$products = \Kotchasan\Model::createQuery()
    ->select('id', 'topic', 'price')
    ->from('product')
    ->execute();

// ✅ GOOD: Use caching for frequently accessed data
$categories = \Kotchasan\Model::createQuery()
    ->from('product_select')
    ->where([['type', 'category_id']])
    ->cacheOn()  // Enable query caching
    ->execute();

// ❌ BAD: Select all columns when you only need a few
$products = \Kotchasan\Model::createQuery()
    ->select()  // Selects all columns
    ->from('product')
    ->execute();

Additional Resources

Summary

This document provided real, working code examples from the NowJS Bookstore project, demonstrating:

  1. Product Management - Complete CRUD with relations and DataTable
  2. User Management - User lifecycle, permissions, and bulk actions
  3. API Development - Using Gcms\Table base class for rapid API development
  4. Complete Examples - End-to-end workflows showing how components work together

All code examples are taken directly from the actual project and are production-tested. Use them as templates for your own modules and applications.