Kotchasan Framework Documentation

Kotchasan Framework Documentation

ตัวอย่างการพัฒนาเว็บแอปพลิเคชันจริง

TH 06 Feb 2026 07:31

ตัวอย่างการพัฒนาเว็บแอปพลิเคชันจริง

คู่มือนี้นำเสนอตัวอย่างโค้ดที่ใช้งานจริงจากโปรเจ็กต์ NowJS Bookstore แสดงการพัฒนาเว็บแอปพลิเคชันด้วย Kotchasan Framework

หมายเหตุ: ตัวอย่างโค้ดทั้งหมดในเอกสารนี้ถูกนำมาจากโปรเจ็กต์จริงและผ่านการทดสอบ 100%

สารบัญ

  1. ระบบจัดการสินค้า
  2. ระบบจัดการผู้ใช้
  3. การพัฒนา API ด้วย Gcms\Table
  4. ตัวอย่างการใช้งานแบบครบวงจร

ระบบจัดการสินค้า

บทนี้แสดงระบบจัดการสินค้าจริงพร้อมการทำ 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();

แหล่งข้อมูลเพิ่มเติม

สรุป

เอกสารนี้ให้ตัวอย่างโค้ดจริงที่ใช้งานได้จากโปรเจ็กต์ NowJS Bookstore แสดง:

  1. การจัดการสินค้า - CRUD สมบูรณ์พร้อมความสัมพันธ์และ DataTable
  2. การจัดการผู้ใช้ - วงจรชีวิตผู้ใช้, สิทธิ์, และการทำงานแบบกลุ่ม
  3. การพัฒนา API - การใช้คลาส base Gcms\Table สำหรับการพัฒนา API อย่างรวดเร็ว
  4. ตัวอย่างครบวงจร - กระบวนการทำงานแบบ end-to-end แสดงการทำงานร่วมกันของส่วนประกอบต่างๆ

ตัวอย่างโค้ดทั้งหมดนำมาจากโปรเจ็กต์จริงและผ่านการทดสอบในสภาพแวดล้อมจริง ใช้เป็นแม่แบบสำหรับโมดูลและแอปพลิเคชันของคุณเอง