Kotchasan Framework Documentation
ตัวอย่างการพัฒนาเว็บแอปพลิเคชันจริง
ตัวอย่างการพัฒนาเว็บแอปพลิเคชันจริง
คู่มือนี้นำเสนอตัวอย่างโค้ดที่ใช้งานจริงจากโปรเจ็กต์ NowJS Bookstore แสดงการพัฒนาเว็บแอปพลิเคชันด้วย Kotchasan Framework
หมายเหตุ: ตัวอย่างโค้ดทั้งหมดในเอกสารนี้ถูกนำมาจากโปรเจ็กต์จริงและผ่านการทดสอบ 100%
สารบัญ
ระบบจัดการสินค้า
บทนี้แสดงระบบจัดการสินค้าจริงพร้อมการทำ CRUD, ความสัมพันธ์ของข้อมูล และการใช้งาน DataTable
โครงสร้างฐานข้อมูล
-- ตารางสินค้า
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`)
);
-- รายละเอียดสินค้า (ความสัมพันธ์)
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
);
-- ตัวเลือกสินค้า (หมวดหมู่, ผู้แต่ง, ฯลฯ)
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
โมเดลนี้จัดการการทำ CRUD สำหรับสินค้าพร้อมความสัมพันธ์
/**
* Product Model
* @filesource modules/product/models/product.php
*/
namespace Product\Product;
use Kotchasan\Model;
class Model extends \Kotchasan\Model
{
/**
* ดึงข้อมูลสินค้าพร้อมความสัมพันธ์
*
* @param int $id รหัสสินค้า (0 สำหรับสินค้าใหม่)
* @return array ข้อมูลสินค้าพร้อมตัวเลือกและความสัมพันธ์
*/
public static function get($id)
{
$product = [];
if ($id === 0) {
// สินค้าใหม่ - คืนค่าโครงสร้างว่าง
$product['data'] = (object) [
'id' => 0,
'topic' => '',
'stock' => 0,
'price' => 0,
'published' => 1,
'author_id' => [],
'category_id' => []
];
$product['options'] = self::getOptions();
$product['relations'] = [];
return $product;
}
// ดึงข้อมูลสินค้าที่มีอยู่
$product['data'] = static::createQuery()
->select()
->from('product')
->where([['id', $id]])
->first();
if ($product['data']) {
$product['options'] = self::getOptions();
// โหลดความสัมพันธ์ของสินค้า (ผู้แต่ง, หมวดหมู่, ฯลฯ)
$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;
}
/**
* ดึงตัวเลือกสำหรับฟอร์มสินค้า
* คืนค่าผู้แต่ง, หมวดหมู่, ฯลฯ
*
* @return array ตัวเลือกจัดกลุ่มตามประเภท
*/
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;
}
/**
* บันทึกข้อมูลสินค้าพร้อมความสัมพันธ์
*
* @param \Kotchasan\DB $db การเชื่อมต่อฐานข้อมูล
* @param int $id รหัสสินค้า (0 สำหรับสินค้าใหม่)
* @param array $save ข้อมูลสินค้า
* @param array $relations ข้อมูลที่เกี่ยวข้อง (ผู้แต่ง, หมวดหมู่)
* @return int รหัสสินค้า
*/
public static function save($db, $id, $save, $relations)
{
// 1. บันทึกข้อมูลสินค้าหลัก
if ($id > 0) {
// อัปเดตสินค้าที่มีอยู่
$db->update('product', [['id', $id]], $save);
} else {
// เพิ่มสินค้าใหม่
$id = $db->insert('product', $save);
}
// 2. ลบความสัมพันธ์ทั้งหมดที่มีอยู่
$db->delete('product_details', [['product_id', $id]], 0);
// 3. เพิ่มความสัมพันธ์ใหม่
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)
โมเดลนี้ให้ข้อมูลสำหรับตารางรายการสินค้า
/**
* Products Model
* @filesource modules/product/models/products.php
*/
namespace Product\Products;
use Kotchasan\Model;
class Model extends \Kotchasan\Model
{
/**
* สร้างคำสั่ง Query สินค้าสำหรับ DataTable
* รองรับการค้นหาและกรองข้อมูล
*
* @param array $params พารามิเตอร์การค้นหา (search, published, ฯลฯ)
* @return \Kotchasan\QueryBuilder\QueryBuilderInterface
*/
public static function toDataTable($params)
{
$where = [];
// กรองตามสถานะเผยแพร่
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);
// ค้นหาในหลายฟิลด์ (เงื่อนไข OR)
if (!empty($params['search'])) {
$search = '%' . $params['search'] . '%';
$query->where([
['topic', 'LIKE', $search],
['isbn', 'LIKE', $search],
['product_no', 'LIKE', $search]
], 'OR');
}
return $query;
}
/**
* ลบสินค้าตาม IDs
* ลบข้อมูล product_details ที่เกี่ยวข้องด้วย
*
* @param int|array $ids รหัสสินค้าหรืออาร์เรย์ของรหัส
* @return int จำนวนสินค้าที่ลบ
*/
public static function remove($ids)
{
if (empty($ids)) {
return 0;
}
// 1. ลบรายละเอียดสินค้า (ความสัมพันธ์)
static::createQuery()
->delete('product_details')
->where([['product_id', $ids]])
->execute();
// 2. ลบสินค้า
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);
}
/**
* อัปเดตฟิลด์สถานะสินค้า
* ใช้สำหรับอัปเดตสถานะแบบกลุ่ม (published, recommend, hot, new)
*
* @param int|array $ids รหัสสินค้า
* @param string $column คอลัมน์ที่จะอัปเดต
* @param int $value ค่าใหม่ (0 หรือ 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 (แสดงผลหน้าเว็บ)
โมเดลนี้ใช้สำหรับแสดงสินค้าบนเว็บไซต์สาธารณะ
/**
* 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;
/**
* สร้างคำสั่ง Query รายการสินค้าพร้อมตัวกรอง
*
* @param array $params พารามิเตอร์การค้นหา
* @return self
*/
public static function create($params)
{
$obj = new static();
// เงื่อนไขพื้นฐาน
$where = [
['P.published', 1] // เฉพาะสินค้าที่เผยแพร่
];
// ตัวกรองเพิ่มเติม
if (!empty($params['recommend'])) {
$where[] = ['P.recommend', 1];
}
if (!empty($params['new'])) {
$where[] = ['P.new', 1];
}
if (!empty($params['hot'])) {
$where[] = ['P.hot', 1];
}
// สร้างคำสั่ง Query พื้นฐาน
$obj->instance = Model::createQuery()
->from('product P')
->where($where)
->cacheOn(); // เปิดใช้งาน query caching
// กรองตามหมวดหมู่ (ด้วย 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;
}
/**
* ดำเนินการ Query และคืนค่าสินค้า
*
* @param int $limit จำนวนสินค้าที่ต้องการ
* @return array รายการสินค้า
*/
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]) // เฉพาะสินค้าที่มีสต็อก
->orderBy('create_date', 'DESC')
->limit($limit)
->fetchAll();
}
}ตัวอย่างการใช้งาน:
// ดึงสินค้าแนะนำใหม่
$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)
Controller นี้สืบทอดจาก Gcms\Table เพื่อให้ API endpoint ที่สมบูรณ์สำหรับการจัดการสินค้า
/**
* 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
{
/**
* คอลัมน์ที่อนุญาตให้เรียงลำดับ (ป้องกัน SQL injection)
*/
protected $allowedSortColumns = [
'id',
'product_no',
'topic',
'isbn',
'price',
'stock',
'published',
'create_date'
];
/**
* ดึงพารามิเตอร์เพิ่มเติมสำหรับการกรองตาราง
*
* @param Request $request
* @param object $login ผู้ใช้ปัจจุบัน
* @return array
*/
protected function getCustomParams(Request $request, $login): array
{
return [
'published' => $request->get('published')->filter('01')
];
}
/**
* ตรวจสอบสิทธิ์
* เฉพาะผู้ใช้ที่มีสิทธิ์จัดการสินค้าเท่านั้น
*
* @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('ต้องการสิทธิ์การเข้าถึง', 403);
}
return true;
}
/**
* สร้างคำสั่ง Query ข้อมูลสำหรับ DataTable
*
* @param array $params
* @param object $login
* @return \Kotchasan\QueryBuilder\QueryBuilderInterface
*/
protected function toDataTable($params, $login = null)
{
return \Product\Products\Model::toDataTable($params);
}
/**
* จัดรูปแบบรายการข้อมูลพร้อมฟิลด์แสดงผลเพิ่มเติม
*
* @param array $datas
* @param object $login
* @return array
*/
protected function formatDatas(array $datas, $login = null): array
{
$data = [];
foreach ($datas as $row) {
// เพิ่มรูปภาพสินค้า
$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;
}
/**
* ดึงตัวกรองสำหรับตาราง
*
* @param array $params
* @param object $login
* @return array
*/
protected function getFilters($params, $login = null)
{
return [
'published' => self::getPublishedOptions()
];
}
/**
* จัดการคำสั่งแก้ไข
* เปลี่ยนเส้นทางไปยังหน้าแก้ไขสินค้า
*
* @param Request $request
* @param object $login
* @return array
*/
protected function handleEditAction(Request $request, $login)
{
if (!ApiController::hasPermission($login, ['can_manage_product'])) {
return $this->errorResponse('ไม่สามารถดำเนินการได้', 403);
}
$id = $request->post('id')->toInt();
return $this->redirectResponse('/product?id=' . $id);
}
/**
* จัดการคำสั่งลบ
* ลบสินค้าที่เลือก
*
* @param Request $request
* @param object $login
* @return array
*/
protected function handleDeleteAction(Request $request, $login)
{
if (!ApiController::canModify($login, ['can_manage_product'])) {
return $this->errorResponse('ไม่สามารถดำเนินการได้', 403);
}
$ids = $request->request('ids', [])->toInt();
$removeCount = \Product\Products\Model::remove($ids);
if (empty($removeCount)) {
return $this->errorResponse('ลบข้อมูลล้มเหลว', 400);
}
// บันทึกการกระทำ
\Index\Log\Model::add(
0,
'Product',
'ลบสินค้า ID(s) : ' . implode(', ', $ids),
$login->id
);
return $this->redirectResponse('reload', 'ลบ ' . $removeCount . ' รายการสำเร็จ');
}
/**
* จัดการคำสั่งสถานะ (published|1, published|0, ฯลฯ)
*
* @param Request $request
* @param object $login
* @return array
*/
protected function handleStatusAction(Request $request, $login)
{
if (!ApiController::canModify($login, ['can_manage_product'])) {
return $this->errorResponse('ไม่สามารถดำเนินการได้', 403);
}
$ids = $request->request('ids', [])->toInt();
$status = $request->request('status')->filter('a-z0-9_');
// แยกวิเคราะห์สถานะ: published_1, recommend_0, ฯลฯ
if (preg_match('/^([a-z]+)_([0-1])$/', $status, $match)) {
$column = $match[1];
$value = (int) $match[2];
// ตรวจสอบคอลัมน์
if (in_array($column, ['published', 'recommend', 'hot', 'new'])) {
\Product\Products\Model::updateStatus($ids, $column, $value);
// บันทึกการกระทำ
\Index\Log\Model::add(
0,
'Product',
'อัปเดตสินค้า ID(s) : ' . implode(', ', $ids) . ' ' . $column . '=' . $value,
$login->id
);
return $this->redirectResponse('reload', 'อัปเดตสำเร็จ');
}
}
return $this->errorResponse('คำสั่งสถานะไม่ถูกต้อง', 400);
}
/**
* ดึงตัวเลือกสถานะเผยแพร่
*
* @return array
*/
public static function getPublishedOptions()
{
return [
['value' => '1', 'text' => 'เปิดใช้งาน'],
['value' => '0', 'text' => 'ปิดใช้งาน']
];
}
}API Endpoints:
GET /api/products - แสดงรายการสินค้าพร้อม pagination
POST /api/products/action - คำสั่งกลุ่ม (edit, delete, status)
GET /api/products/export - ส่งออกสินค้าเป็น CSVระบบจัดการผู้ใช้
บทนี้แสดงการจัดการผู้ใช้จริงพร้อมสิทธิ์ตามบทบาท การจัดการสถานะ และการทำงานแบบกลุ่ม
โครงสร้างฐานข้อมูล
-- ตารางผู้ใช้
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
*/
public static $socialIcons = [
1 => 'icon-facebook',
2 => 'icon-google',
3 => 'icon-line',
4 => 'icon-telegram'
];
/**
* ดึงคลาสไอคอน Social
*
* @param int $social ประเภท Social login
* @return string ชื่อคลาสไอคอน
*/
public static function getSocialIcon($social)
{
return self::$socialIcons[$social] ?? 'icon-user';
}
/**
* สร้างคำสั่ง Query ผู้ใช้สำหรับ DataTable
* รองรับการกรองสถานะและการค้นหา
*
* @param array $params พารามิเตอร์การค้นหา
* @return \Kotchasan\QueryBuilder\QueryBuilderInterface
*/
public static function toDataTable($params)
{
// ตัวกรอง (เงื่อนไข AND)
$where = [];
if (isset($params['status']) && $params['status'] !== '') {
$where[] = ['U.status', (int) $params['status']];
}
// สร้างคำสั่ง 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);
// ค้นหา (เงื่อนไข OR)
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;
}
/**
* ลบผู้ใช้ตาม ID
* ลบไฟล์รูปโปรไฟล์ด้วย
*
* @param int|array $ids รหัสผู้ใช้หรืออาร์เรย์ของรหัส
* @return int จำนวนผู้ใช้ที่ลบ
*/
public static function remove($ids)
{
$remove_ids = [];
// ลบไฟล์สำหรับแต่ละผู้ใช้
foreach ((array) $ids as $id) {
// ป้องกันผู้ดูแลระบบ (ID=1)
if ($id == 1) {
continue;
}
// ลบรูปโปรไฟล์ผู้ใช้
$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;
}
// ลบผู้ใช้จากฐานข้อมูล
return \Kotchasan\DB::create()
->delete('user', [['id', $remove_ids]]);
}
/**
* แปลงผู้ใช้เป็นตัวเลือก select
* ใช้สำหรับ dropdown, multi-select, ฯลฯ
*
* @param array $where เงื่อนไขเพิ่มเติม (ไม่บังคับ)
* @return array อาร์เรย์ของคู่ ['value', 'text']
*/
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
*/
public static $socialTypes = [
0 => 'ลงทะเบียน',
1 => 'Facebook',
2 => 'Google',
3 => 'LINE',
4 => 'Telegram'
];
/**
* คอลัมน์ที่อนุญาตให้เรียงลำดับ
*/
protected $allowedSortColumns = ['id', 'name', 'active', 'create_date', 'status'];
/**
* ดึงพารามิเตอร์เพิ่มเติม
*
* @param Request $request
* @param object $login
* @return array
*/
protected function getCustomParams(Request $request, $login): array
{
return [
'status' => $request->get('status')->number()
];
}
/**
* ตรวจสอบสิทธิ์
* เฉพาะผู้ดูแลระบบเท่านั้นที่สามารถจัดการผู้ใช้
*
* @param Request $request
* @param object $login
* @return true|array
*/
protected function checkAuthorization(Request $request, $login)
{
if (!ApiController::isSuperAdmin($login)) {
return $this->errorResponse('ต้องการสิทธิ์การเข้าถึง', 403);
}
return true;
}
/**
* สร้างคำสั่ง Query ข้อมูลสำหรับ DataTable
*
* @param array $params
* @param object $login
* @return \Kotchasan\QueryBuilder\QueryBuilderInterface
*/
protected function toDataTable($params, $login = null)
{
return \Index\Users\Model::toDataTable($params);
}
/**
* จัดรูปแบบรายการผู้ใช้พร้อมฟิลด์แสดงผลเพิ่มเติม
*
* @param array $datas
* @param object $login
* @return array
*/
protected function formatDatas(array $datas, $login = null): array
{
$data = [];
foreach ($datas as $row) {
// เพิ่มข้อความสถานะ
$row->status_text = self::$cfg->member_status[$row->status] ?? $row->status;
// เพิ่มตัวอักษรย่อชื่อ
$row->initial_name = self::getInitialName($row->name);
// เพิ่ม 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()
];
}
/**
* จัดการคำสั่งลบ
*
* @param Request $request
* @param object $login
* @return array
*/
protected function handleDeleteAction(Request $request, $login)
{
if (!ApiController::isSuperAdmin($login)) {
return $this->errorResponse('ไม่สามารถดำเนินการได้', 403);
}
$ids = $request->request('ids', [])->toInt();
$removeCount = \Index\Users\Model::remove($ids);
if (empty($removeCount)) {
return $this->errorResponse('ลบข้อมูลล้มเหลว', 400);
}
\Index\Log\Model::add(
0,
'Index',
'ลบผู้ใช้ ID(s) : ' . implode(', ', $ids),
$login->id
);
return $this->redirectResponse('reload', 'ลบ ' . $removeCount . ' ผู้ใช้สำเร็จ');
}
/**
* 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 - แสดงรายการผู้ใช้พร้อม pagination
POST /api/users/action - คำสั่งกลุ่ม (edit, delete, activate, ฯลฯ)การพัฒนา API ด้วย Gcms\Table
คลาส Gcms\Table เป็น base controller ที่ให้เฟรมเวิร์ค API สมบูรณ์สำหรับการจัดการข้อมูลแบบตาราง
ภาพรวม Gcms\Table
Gcms\Table เป็น base controller ที่มีความสามารถ:
- Pagination อัตโนมัติ และการเรียงลำดับ
- การจัดการ action แบบไดนามิก ผ่านรูปแบบการตั้งชื่อเมธอด
- จุดเชื่อมต่อสำหรับตรวจสอบสิทธิ์
- รองรับการส่งออก CSV/PDF
- การผสาน DataTable
- ป้องกัน SQL injection
รูปแบบการใช้งานพื้นฐาน
namespace MyModule\MyResource;
use Gcms\Api as ApiController;
use Kotchasan\Http\Request;
class Controller extends \Gcms\Table
{
// 1. กำหนดคอลัมน์ที่สามารถเรียงลำดับ (ป้องกัน SQL injection)
protected $allowedSortColumns = ['id', 'name', 'status', 'created_at'];
// 2. ตรวจสอบสิทธิ์
protected function checkAuthorization(Request $request, $login)
{
if (!ApiController::hasPermission($login, ['can_manage_resource'])) {
return $this->errorResponse('ต้องการสิทธิ์การเข้าถึง', 403);
}
return true;
}
// 3. สร้างคำสั่ง Query ข้อมูล
protected function toDataTable($params, $login = null)
{
return \MyModule\MyResource\Model::toDataTable($params);
}
// 4. เพิ่มพารามิเตอร์เพิ่มเติม
protected function getCustomParams(Request $request, $login): array
{
return [
'status' => $request->get('status')->filter('01'),
'category' => $request->get('category')->toInt()
];
}
// 5. จัดรูปแบบข้อมูลที่ส่งออก
protected function formatDatas(array $datas, $login = null): array
{
foreach ($datas as $row) {
$row->formatted_date = date('d/m/Y', strtotime($row->created_at));
$row->status_label = $row->status ? 'ใช้งาน' : 'ไม่ใช้งาน';
}
return $datas;
}
// 6. Add filters
protected function getFilters($params, $login = null)
{
return [
'status' => [
['value' => '1', 'text' => 'Active'],
['value' => '0', 'text' => 'Inactive']
]
];
}
}ตัวจัดการ Action
Actions จัดการผ่านรูปแบบการตั้งชื่อเมธอด: handle{Action}Action
/**
* จัดการคำสั่งลบ
* เรียกโดย: POST /api/resource/action?action=delete
*/
protected function handleDeleteAction(Request $request, $login)
{
// Check permission
if (!ApiController::canModify($login, ['can_delete_resource'])) {
return $this->errorResponse('ไม่มีสิทธิ์', 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)
ตัวจัดการการส่งออกข้อมูลใช้รูปแบบการตั้งชื่อเดียวกัน: handle{Type}Export
/**
* จัดการการส่งออก CSV (Handle CSV export)
* เรียกใช้โดย: GET /api/resource/export?type=csv
*/
protected function handleCsvExport(Request $request, $login)
{
$params = $this->parseParams($request, $login);
// กำหนดหัวคอลัมน์ CSV (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))
];
};
// ส่งออกเป็น CSV (ส่งไฟล์และจบการทำงาน)
$this->exportToCsv($params, $login, $headers, $rowFormatter, 'resources');
}รูปแบบการตรวจสอบสิทธิ์
// ตรวจสอบว่าผู้ใช้มีสิทธิ์เฉพาะ
if (ApiController::hasPermission($login, ['permission_name'])) {
// ผู้ใช้มีสิทธิ์
}
// ตรวจสอบว่าผู้ใช้สามารถแก้ไข (ไม่อยู่ในโหมดDemo)
if (ApiController::canModify($login, ['permission_name'])) {
// ผู้ใช้สามารถแก้ไข
}
// ตรวจสอบว่าผู้ใช้เป็นแอดมิน
if (ApiController::isAdmin($login)) {
// ผู้ใช้เป็นแอดมิน
}
// ตรวจสอบว่าผู้ใช้เป็นผู้ดูแลระบบ
if (ApiController::isSuperAdmin($login)) {
// ผู้ใช้เป็นผู้ดูแลระบบ
}เมธอดการตอบกลับ (Response Methods)
// ตอบกลับสำเร็จ (HTTP 200)
return $this->successResponse($data, 'การทำงานสำเร็จ');
// ตอบกลับข้อผิดพลาด
return $this->errorResponse('ข้อความแจ้งข้อผิดพลาด', 400);
// เปลี่ยนเส้นทาง (สั่งให้ Client รีโหลดหรือเปลี่ยนหน้า)
return $this->redirectResponse('reload', 'ข้อความสำเร็จ');
return $this->redirectResponse('/path/to/page', 'กำลังเปลี่ยนหน้า...');ตัวอย่างการใช้งานแบบครบวงจร
ส่วนนี้แสดงตัวอย่างที่สมบูรณ์แบบ End-to-End แสดงให้เห็นว่าส่วนประกอบต่างๆ ทำงานร่วมกันอย่างไร
ตัวอย่างที่ 1: กระบวนการจัดการสินค้า
กระบวนการครบถ้วนตั้งแต่การแสดงรายการไปจนถึงการแก้ไขพร้อมการตรวจสอบสิทธิ์
// 1. แสดงรายการสินค้า (GET /api/products?page=1&pageSize=25&published=1)
{
"data": [
{
"id": 1,
"topic": "พื้นฐาน PHP",
"isbn": "978-1234567890",
"price": "299.00",
"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. บันทึกสินค้า (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' => 'ชื่อสินค้าที่อัปเดต',
'price' => 349.00,
'stock' => 15,
'published' => 1
];
$relations = [
'title' => ['รายการที่เกี่ยวข้อง 1', 'รายการที่เกี่ยวข้อง 2'],
'author_id' => [5, 8],
'category_id' => [2, 3]
];
$db = \Kotchasan\DB::create();
$productId = \Product\Product\Model::save($db, 1, $save, $relations);
// 3. อัปเดตสถานะ (POST /api/products/action)
// ตั้งค่า published=0 สำหรับสินค้า 1, 2, 3
// 4. ลบสินค้า (POST /api/products/action)
// ลบสินค้า 5, 6, 7 และความสัมพันธ์ตัวอย่างที่ 2: การลงทะเบียนและเปิดใช้งานผู้ใช้
วงจรชีวิตผู้ใช้ที่สมบูรณ์ตั้งแต่การลงทะเบียนจนถึงการเปิดใช้งาน
// 1. ลงทะเบียนผู้ใช้
$userData = [
'username' => 'somchai@example.com',
'password' => password_hash('รหัสผ่านปลอดภัย123', PASSWORD_DEFAULT),
'name' => 'สมชาย ใจดี',
'phone' => '081-234-5678',
'status' => 0,
'active' => 0,
'activatecode' => md5(uniqid() . time())
];
$userId = \Kotchasan\DB::create()->insert('user', $userData);
// 2. ส่งอีเมลเปิดใช้งาน
\Index\Email\Model::sendActivation(
$userData['username'],
WEB_URL . 'activate?id=' . $userData['activatecode'],
$userData['name']
);
// 3. แอดมินดูผู้ใช้ที่รอดำเนินการ
$params = ['status' => 0, 'search' => '', 'page' => 1, 'pageSize' => 25];
$query = \Index\Users\Model::toDataTable($params);
$users = $query->execute()->fetchAll();
// 4. แอดมินเปิดใช้งานผู้ใช้
\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]
]);ตัวอย่างที่ 3: การสร้าง API Controller แบบกำหนดเอง
ตัวอย่างที่สมบูรณ์ของการสร้าง 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', 'ทำเครื่องหมายคำสั่งซื้อว่าจัดส่งแล้ว');
}
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">รอดำเนินการ</span>',
'processing' => '<span class="badge badge-info">กำลังดำเนินการ</span>',
'shipped' => '<span class="badge badge-primary">จัดส่งแล้ว</span>',
'delivered' => '<span class="badge badge-success">ส่งถึงปลายทาง</span>',
'cancelled' => '<span class="badge badge-danger">ยกเลิก</span>'
];
return $badges[$status] ?? $status;
}
}การเชื่อมต่อ JavaScript ฝั่ง Frontend:
// ดึงรายการคำสั่งซื้อ (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();
}
}
// ส่งออกเป็น CSV (Export to CSV)
function exportOrders(filters = {}) {
const params = new URLSearchParams({
type: 'csv',
...filters
});
window.location.href = `/api/orders/export?${params}`;
}แนวทางปฏิบัติที่ดี
1. ใช้ Prepared Statements เสมอ
// ✅ ดี: ใช้ QueryBuilder (ใช้ prepared statements อัตโนมัติ)
$products = \Kotchasan\Model::createQuery()
->from('product')
->where([['topic', 'LIKE', '%' . $search . '%']])
->execute();
// ❌ ไม่ดี: SQL โดยตรงกับข้อมูลจากผู้ใช้
$products = \Kotchasan\DB::create()->customQuery(
"SELECT * FROM product WHERE topic LIKE '%" . $_GET['search'] . "%'"
);2. ตรวจสอบข้อมูลจากผู้ใช้
// ✅ ดี: ใช้ Input filters
$id = $request->get('id')->toInt(); // ตรวจสอบเป็นจำนวนเต็ม
$email = $request->post('email')->email(); // ตรวจสอบรูปแบบอีเมล
$status = $request->get('status')->filter('01'); // อนุญาตเฉพาะ '0' หรือ '1'
// ❌ BAD: Using raw input
$id = $_GET['id'];
$email = $_POST['email'];3. ตรวจสอบสิทธิ์
// ✅ ดี: ตรวจสอบสิทธิ์ก่อนทำงานที่สำคัญ
if (!ApiController::canModify($login, ['can_delete_product'])) {
return $this->errorResponse('ไม่มีสิทธิ์', 403);
}
// ❌ BAD: No permission check
$ids = $request->request('ids')->toInt();
\Product\Products\Model::remove($ids);4. บันทึกการกระทำสำคัญ
// ✅ ดี: บันทึกการกระทำที่ทำลายข้อมูล
\Index\Log\Model::add(
0,
'Product',
'ลบสินค้า ID(s) : ' . implode(', ', $ids),
$login->id
);
// Log user actions for audit trail
\Index\Log\Model::add(
$userid,
'Auth',
'User login successful',
$userid
);5. จัดการข้อผิดพลาดอย่างเหมาะสม
// ✅ ดี: Try-catch พร้อมข้อความที่เข้าใจได้
try {
$result = \Product\Product\Model::save($db, $id, $data, $relations);
return $this->successResponse(['id' => $result], 'บันทึกสินค้าสำเร็จ');
} catch (\Exception $e) {
// Log the error
error_log('บันทึกสินค้าล้มเหลว: ' . $e->getMessage());
// Return user-friendly message
return $this->errorResponse('ไม่สามารถบันทึกสินค้า กรุณาลองใหม่', 500);
}6. ใช้ Transaction สำหรับการทำงานที่เกี่ยวข้อง
// ✅ ดี: ใช้ transaction สำหรับการทำงานหลายตาราง
$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)
// ✅ ดี: เลือกเฉพาะคอลัมน์ที่จำเป็น
$products = \Kotchasan\Model::createQuery()
->select('id', 'topic', 'price')
->from('product')
->execute();
// ✅ ดี: ใช้การแคชสำหรับข้อมูลที่เรียกใช้บ่อย
$categories = \Kotchasan\Model::createQuery()
->from('product_select')
->where([['type', 'category_id']])
->cacheOn() // เปิดใช้งาน query caching
->execute();
// ❌ ไม่ดี: เลือกทุกคอลัมน์เมื่อใช้เพียงบางส่วน
$products = \Kotchasan\Model::createQuery()
->select() // เลือกคอลัมน์ทั้งหมด
->from('product')
->execute();แหล่งข้อมูลเพิ่มเติม
- เอกสาร Model - การจัดการฐานข้อมูลและ QueryBuilder
- เอกสาร Controller - พื้นฐาน MVC controller
- เอกสาร API Controller - การพัฒนา API
- เอกสาร View - การแสดงผล Template
- เอกสารฐานข้อมูล - คุณสมบัติฐานข้อมูลขั้นสูง
สรุป
เอกสารนี้ให้ตัวอย่างโค้ดจริงที่ใช้งานได้จากโปรเจ็กต์ NowJS Bookstore แสดง:
- การจัดการสินค้า - CRUD สมบูรณ์พร้อมความสัมพันธ์และ DataTable
- การจัดการผู้ใช้ - วงจรชีวิตผู้ใช้, สิทธิ์, และการทำงานแบบกลุ่ม
- การพัฒนา API - การใช้คลาส base
Gcms\Tableสำหรับการพัฒนา API อย่างรวดเร็ว - ตัวอย่างครบวงจร - กระบวนการทำงานแบบ end-to-end แสดงการทำงานร่วมกันของส่วนประกอบต่างๆ
ตัวอย่างโค้ดทั้งหมดนำมาจากโปรเจ็กต์จริงและผ่านการทดสอบในสภาพแวดล้อมจริง ใช้เป็นแม่แบบสำหรับโมดูลและแอปพลิเคชันของคุณเอง