Kotchasan Framework Documentation
Practical Web Development Examples
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
- Product Management System
- User Management System
- API Development with Gcms\Table
- 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 CSVUser 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 relationsExample 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);
}6. Use Transactions for Related Operations
// ✅ 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
- Model Documentation - Database operations and QueryBuilder
- Controller Documentation - MVC controller basics
- API Controller Documentation - API development
- View Documentation - Template rendering
- Database Documentation - Advanced database features
Summary
This document provided real, working code examples from the NowJS Bookstore project, demonstrating:
- Product Management - Complete CRUD with relations and DataTable
- User Management - User lifecycle, permissions, and bulk actions
- API Development - Using
Gcms\Tablebase class for rapid API development - 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.