Kotchasan Framework Documentation

Kotchasan Framework Documentation

Basic Web Development Guide with Kotchasan

EN 07 Feb 2026 02:20

Basic Web Development Guide with Kotchasan

Introduction

This guide introduces basic web development with the Kotchasan Framework, covering CRUD operations, user management, authentication, form handling, validation, and file uploads.

Table of Contents

  1. Project Setup
  2. Creating CRUD Operations
  3. Authentication System
  4. Form Management
  5. Data Validation
  6. File Upload
  7. Complete Web Page Examples
  8. Best Practices

Project Setup

1. Project Structure

myproject/
├── modules/
│   └── blog/
│       ├── controllers/
│       ├── models/
│       ├── views/
│       └── template/
├── settings/
│   ├── config.php
│   └── database.php
├── datas/
│   ├── cache/
│   ├── logs/
│   └── uploads/
└── index.php

2. Database Configuration

// settings/database.php
return [
    'default' => [
        'driver' => 'mysql',
        'host' => 'localhost',
        'database' => 'myblog',
        'username' => 'root',
        'password' => '',
        'charset' => 'utf8mb4',
        'collation' => 'utf8mb4_unicode_ci',
        'prefix' => 'blog_'
    ]
];

3. Creating Tables

-- Users table
CREATE TABLE blog_users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    password VARCHAR(255) NOT NULL,
    role ENUM('admin', 'user') DEFAULT 'user',
    status ENUM('active', 'inactive') DEFAULT 'active',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- Posts table
CREATE TABLE blog_posts (    i
d INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    title VARCHAR(200) NOT NULL,
    content TEXT NOT NULL,
    status ENUM('draft', 'published') DEFAULT 'draft',
    views INT DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES blog_users(id) ON DELETE CASCADE
);

-- Categories table
CREATE TABLE blog_categories (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    slug VARCHAR(100) UNIQUE NOT NULL,
    description TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Post-Category relationship table
CREATE TABLE blog_post_categories (
    post_id INT NOT NULL,
    category_id INT NOT NULL,
    PRIMARY KEY (post_id, category_id),
    FOREIGN KEY (post_id) REFERENCES blog_posts(id) ON DELETE CASCADE,
    FOREIGN KEY (category_id) REFERENCES blog_categories(id) ON DELETE CASCADE
);

Creating CRUD Operations

1. Model for CRUD

// modules/blog/models/post.php
namespace Blog\Post;

use Kotchasan\Model;

class Model extends \Kotchasan\Model
{
    protected $table = 'posts';

    /**
     * Get all posts (Read)
     */
    public function getAllPosts($limit = 10, $offset = 0)
    {
        return $this->db->select('p.*', 'u.name as author_name')
            ->from($this->table . ' p')
            ->leftJoin('users u', 'u.id', 'p.user_id')
            ->where('p.status', '=', 'published')
            ->orderBy('p.created_at', 'DESC')
            ->limit($limit, $offset)
            ->fetchAll();
    }

    /**
     * Get post by ID (Read)
     */
    public function getPostById($id)
    {
        $post = $this->db->select('p.**', 'u.name as author_name')
            ->from($this->table . ' p')
            ->leftJoin('users u', 'u.id', 'p.user_id')
            ->where('p.id', '=', $id)
            ->first();

        if ($post) {
            // Increment view count
            $this->db->update($this->table)
                ->set(['views' => 'views + 1'])
                ->where('id', '=', $id)
                ->execute();
        }

        return $post;
    }

    /**
     * Create new post (Create)
     */
    public function createPost($data)
    {
        // Validate data
        $this->validatePostData($data);

        $data['created_at'] = date('Y-m-d H:i:s');
        $data['status'] = $data['status'] ?? 'draft';

        $this->db->insert($this->table)
            ->values($data)
            ->execute();

        return $this->db->lastInsertId();
    }

    /**
     * Update post (Update)
     */
    public function updatePost($id, $data)
    {
        $this->validatePostData($data);

        $data['updated_at'] = date('Y-m-d H:i:s');

        return $this->db->update($this->table)
            ->set($data)
            ->where('id', '=', $id)
            ->execute();
    }

    /**
     * Delete post (Delete)
     */
    public function deletePost($id)
    {
        return $this->db->delete($this->table)
            ->where('id', '=', $id)
            ->execute();
    }

    /**
     * Validate post data
     */
    private function validatePostData($data)
    {
        if (empty($data['title'])) {
            throw new \Exception('Title is required');
        }

        if (empty($data['content'])) {
            throw new \Exception('Content is required');
        }

        if (empty($data['user_id'])) {
            throw new \Exception('Author information not found');
        }
    }

    /**
     * Search posts
     */
    public function searchPosts($keyword)
    {
        return $this->db->select('p.*', 'u.name as author_name')
            ->from($this->table . ' p')
            ->leftJoin('users u', 'u.id', 'p.user_id')
            ->where('p.status', '=', 'published')
            ->where('p.title', 'LIKE', "%{$keyword}%")
            ->orWhere('p.content', 'LIKE', "%{$keyword}%")
            ->orderBy('p.created_at', 'DESC')
            ->fetchAll();
    }
}

2.Controller for CRUD

// modules/blog/controllers/post.php
namespace Blog\Post;

use Gcms\Login;
use Kotchasan\Http\Request;

class Controller extends \Kotchasan\Controller
{
    /**
     * Display post list
     */
    public function render(Request $request)
    {
        $model = new Model();
        $page = $request->request('page')->toInt() ?: 1;
        $limit = 10;
        $offset = ($page - 1)  $limit;

        $posts = $model->getAllPosts($limit, $offset);

        $view = new View();
        return $view->render($posts, $page);
    }

    /**
     * Display single post
     */
    public function view(Request $request)
    {
        $id = $request->request('id')->toInt();

        if ($id <= 0) {
            return new \Kotchasan\Http\NotFound();
        }

        $model = new Model();
        $post = $model->getPostById($id);

        if (!$post) {
            return new \Kotchasan\Http\NotFound();
        }

        $view = new View();
        return $view->viewPost($post);
    }

    /
      Display create/edit form
     */
    public function form(Request $request)
    {
        // Check login
        $login = Login::isMember();
        if (!$login) {
            return new \Kotchasan\Http\NotFound();
        }

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

        if ($id > 0) {
            $model = new Model();
            $post = $model->getPostById($id);

            // Check edit permission
            if (!$post || ($post['user_id'] != $login['id'] && $login['role'] != 'admin')) {
                return new \Kotchasan\Http\NotFound();
            }
        }

        $view = new View();
        return $view->form($post, $login);
    }
}

3. View for CRUD

// modules/blog/views/post.php
namespace Blog\Post;

use Kotchasan\Html;
use Kotchasan\Http\Request;

class View extends \Gcms\View
{
    /**
     * Display post list
     */
    public function render($posts, $currentPage)
    {
        $container = Html::create('div', ['class' => 'blog-container']);

        // Header
        $container->add('h1', [], 'Our Blog');

        // Search Form
        $searchForm = $container->add('form', [
            'method' => 'GET',
            'class' => 'search-form'
        ]);
        $searchForm->add('input', [
            'type' => 'text',
            'name' => 'search',
            'placeholder' => 'Search posts...',
            'value' => $_GET['search'] ?? ''
        ]);
        $searchForm->add('button', ['type' => 'submit'], 'Search');

        // Post List
        if (empty($posts)) {
            $container->add('p', [], 'No posts found');
        } else {
            foreach ($posts as $post) {
                $article = $container->add('article', ['class' => 'post-item']);

                $article->add('h2')->add('a', [
                    'href' => 'index.php?module=blog&page=post&action=view&id=' . $post['id']
                ], $post['title']);

                $meta = $article->add('div', ['class' => 'post-meta']);
                $meta->add('span', [], 'By ' . $post['author_name']);
                $meta->add('span', [], ' | ' . date('d/m/Y H:i', strtotime($post['created_at'])));
                $meta->add('span', [], ' | Views: ' . number_format($post['views']));

                $content = substr(strip_tags($post['content']), 0, 200);
                $article->add('p', [], $content . '...');

                $article->add('a', [
                    'href' => 'index.php?module=blog&page=post&action=view&id=' . $post['id'],
                    'class' => 'read-more'
                ], 'Read more');
            }
        }

        // Pagination
        $this->renderPagination($container, $currentPage);

        return $container->render();
    }

    /**
     * Display single post
     */
    public function viewPost($post)
    {
        $container = Html::create('div', ['class' => 'post-container']);

        $container->add('h1', [], $post['title']);

        $meta = $container->add('div', ['class' => 'post-meta']);
        $meta->add('span', [], 'By ' . $post['author_name']);
        $meta->add('span', [], ' | ' . date('d/m/Y H:i', strtotime($post['created_at'])));
        $meta->add('span', [], ' | Views: ' . number_format($post['views']));

        $container->add('div', ['class' => 'post-content'], $post['content']);

        // Back button
        $container->add('a', [
            'href' => 'index.php?module=blog&page=post',
            'class' => 'back-button'
        ], '← Back to List');

        return $container->render();
    }
}

Authentication System

1. User Model

// modules/blog/models/user.php
namespace Blog\User;

use Kotchasan\Model;
use Kotchasan\Password;

class Model extends \Kotchasan\Model
{
    protected $table = 'users';

    /**
     * Authenticate user
     */
    public function authenticate($email, $password)
    {
        $user = $this->db->select()
            ->from($this->table)
            ->where('email', '=', $email)
            ->where('status', '=', 'active')
            ->first();

        if ($user && password_verify($password, $user['password'])) {
            // Update last login time
            $this->db->update($this->table)
                ->set(['last_login' => date('Y-m-d H:i:s')])
                ->where('id', '=', $user['id'])
                ->execute();

            return $user;
        }

        return false;
    }

    /**
     * Register new user
     */
    public function register($data)
    {
        // Check for duplicate email
        $existing = $this->db->select('id')
            ->from($this->table)
            ->where('email', '=', $data['email'])
            ->first();

        if ($existing) {
            throw new \Exception('Email already exists');
        }

        // Hash password
        $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
        $data['created_at'] = date('Y-m-d H:i:s');
        $data['role'] = 'user';
        $data['status'] = 'active';

        $this->db->insert($this->table)
            ->values($data)
            ->execute();

        return $this->db->lastInsertId();
    }

    /**
     * Get user by ID
     */
    public function getUserById($id)
    {
        return $this->db->select()
            ->from($this->table)
            ->where('id', '=', $id)
            ->where('status', '=', 'active')
            ->first();
    }

    /**
     * Update profile
     */
    public function updateProfile($id, $data)
    {
        // Don't allow editing email and password here
        unset($data['email'], $data['password'], $data['role']);

        $data['updated_at'] = date('Y-m-d H:i:s');

        return $this->db->update($this->table)
            ->set($data)
            ->where('id', '=', $id)
            ->execute();
    }
}

2. Login Controller

// modules/blog/controllers/login.php
namespace Blog\Login;

use Kotchasan\Http\Request;
use Kotchasan\Session;

class Controller extends \Kotchasan\Controller
{
    /**
     * Display login form
     */
    public function render(Request $request)
    {
        // Redirect if already logged in
        if (Session::get('user_id')) {
            header('Location: index.php?module=blog&page=dashboard');
            exit;
        }

        $view = new View();
        return $view->loginForm();
    }

    /**
     * Authenticate
     */
    public function authenticate(Request $request)
    {
        if ($request->initSession() && $request->isSafe()) {
            try {
                $email = $request->post('email')->toString();
                $password = $request->post('password')->toString();

                if (empty($email) || empty($password)) {
                    throw new \Exception('Please enter email and password');
                }

                $model = new \Blog\User\Model();
                $user = $model->authenticate($email, $password);

                if ($user) {
                    // Record session
                    Session::set('user_id', $user['id']);
                    Session::set('user_name', $user['name']);
                    Session::set('user_role', $user['role']);

                    return [
                        'alert' => 'Login successful',
                        'location' => 'index.php?module=blog&page=dashboard'
                    ];
                } else {
                    throw new \Exception('Incorrect email or password');
                }

            } catch (\Exception $e) {
                return [
                    'alert' => $e->getMessage(),
                    'input' => 'email'
                ];
            }
        }

        return [
            'alert' => 'Invalid login request'
        ];
    }

    /**
     * Logout
     */
    public function logout(Request $request)
    {
        Session::destroy();
        header('Location: index.php?module=blog&page=login');
        exit;
    }
}

3. Register Controller

// modules/blog/controllers/register.php
namespace Blog\Register;

use Kotchasan\Http\Request;

class Controller extends \Kotchasan\Controller
{
    /**
     * Display register form
     */
    public function render(Request $request)
    {
        $view = new View();
        return $view->registerForm();
    }

    /**
     * Register new user
     */
    public function submit(Request $request)
    {
        if ($request->initSession() && $request->isSafe()) {
            try {
                $data = [
                    'name' => $request->post('name')->toString(),
                    'email' => $request->post('email')->toString(),
                    'password' => $request->post('password')->toString(),
                ];

                $confirmPassword = $request->post('confirm_password')->toString();

                // ตรวจสอบข้อมูล
                if (empty($data['name'])) {
                    throw new \Exception('Please enter your name.');
                }

                if (empty($data['email']) || !filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
                    throw new \Exception('Please enter a valid email address.');
                }

                if (strlen($data['password']) < 6) {
                    throw new \Exception('Password must be at least 6 characters long.');
                }

                if ($data['password'] !== $confirmPassword) {
                    throw new \Exception('Passwords do not match.');
                }

                $model = new \Blog\User\Model();
                $userId = $model->register($data);

                return [
                    'alert' => 'Registration successful',
                    'location' => 'index.php?module=blog&page=login'
                ];

            } catch (\Exception $e) {
                return [
                    'alert' => $e->getMessage(),
                    'input' => 'name'
                ];
            }
        }

        return [
            'alert' => 'The membership registration is invalid.'
        ];
    }
}

Form Management

1. Login Form

// modules/blog/views/login.php
namespace Blog\Login;

use Kotchasan\Html;

class View extends \Gcms\View
{
    public function loginForm()
    {
        $container = Html::create('div', ['class' => 'login-container']);

        $container->add('h2', [], 'Login');

        $form = $container->add('form', [
            'id' => 'login_form',
            'class' => 'login-form',
            'action' => 'index.php/blog/login/authenticate',
            'onsubmit' => 'doFormSubmit',
            'ajax' => true,
            'token' => true
        ]);

        // Email
        $group = $form->add('div', ['class' => 'form-group']);
        $group->add('label', [], 'Email');
        $group->add('input', [
            'type' => 'email',
            'name' => 'email',
            'required' => true,
            'placeholder' => 'Enter email'
        ]);

        // Password
        $group = $form->add('div', ['class' => 'form-group']);
        $group->add('label', [], 'Password');
        $group->add('input', [
            'type' => 'password',
            'name' => 'password',
            'required' => true,
            'placeholder' => 'Enter password'
        ]);

        // Submit button
        $form->add('button', [
            'type' => 'submit',
            'class' => 'btn btn-primary'
        ], 'Login');

        // Register link
        $container->add('p')->add('a', [
            'href' => 'index.php?module=blog&page=register'
        ], 'No account? Register');

        return $container->render();
    }
}

2. Create/Edit Post Form

// modules/blog/views/post.php (Add method)
public function form($post, $user)
{
    $isEdit = !empty($post);

    $container = Html::create('div', ['class' => 'post-form-container']);

    $container->add('h2', [], $isEdit ? 'Edit Post' : 'Create New Post');

    $form = $container->add('form', [
        'id' => 'post_form',
        'class' => 'post-form',
        'action' => 'index.php/blog/post/save',
        'onsubmit' => 'doFormSubmit',
        'ajax' => true,
        'token' => true
    ]);

    // ID (for edit)
    if ($isEdit) {
        $form->add('input', [
            'type' => 'hidden',
            'name' => 'id',
            'value' => $post['id']
        ]);
    }

    // Title
    $group = $form->add('div', ['class' => 'form-group']);
    $group->add('label', [], 'Title');
    $group->add('input', [
        'type' => 'text',
        'name' => 'title',
        'value' => $post['title'] ?? '',
        'required' => true,
        'placeholder' => 'Enter post title'
    ]);

    // Content
    $group = $form->add('div', ['class' => 'form-group']);
    $group->add('label', [], 'Content');
    $group->add('textarea', [
        'name' => 'content',
        'required' => true,
        'rows' => 10,
        'placeholder' => 'Enter post content'
    ], $post['content'] ?? '');

    // Status
    $group = $form->add('div', ['class' => 'form-group']);
    $group->add('label', [], 'Status');
    $select = $group->add('select', ['name' => 'status']);
    $select->add('option', [
        'value' => 'draft',
        'selected' => ($post['status'] ?? 'draft') === 'draft'
    ], 'Draft');
    $select->add('option', [
        'value' => 'published',
        'selected' => ($post['status'] ?? '') === 'published'
    ], 'Published');

    // Submit buttons
    $buttonGroup = $form->add('div', ['class' => 'button-group']);
    $buttonGroup->add('button', [
        'type' => 'submit',
        'class' => 'btn btn-primary'
    ], $isEdit ? 'Update' : 'Save');

    $buttonGroup->add('a', [
        'href' => 'index.php?module=blog&page=dashboard',
        'class' => 'btn btn-secondary'
    ], 'Cancel');

    return $container->render();
}
}

Data Validation

1. Validation Helper

// modules/blog/models/validator.php
namespace Blog\Validator;

class Model
{
    /**
     * Validate email
     */
    public static function validateEmail($email)
    {
        if (empty($email)) {
            throw new \Exception('Email is required');
        }

        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new \Exception('Invalid email format');
        }

        return true;
    }

    /**
     * Validate password
     */
    public static function validatePassword($password, $confirmPassword = null)
    {
        if (empty($password)) {
            throw new \Exception('Password is required');
        }

        if (strlen($password) < 6) {
            throw new \Exception('Password must be at least 6 characters');
        }

        if ($confirmPassword !== null && $password !== $confirmPassword) {
            throw new \Exception('Passwords do not match');
        }

        return true;
    }

    /**
     * Validate text
     */
    public static function validateText($text, $fieldName, $minLength = 1, $maxLength = null)
    {
        if (empty($text)) {
            throw new \Exception("{$fieldName} is required");
        }

        $length = mb_strlen($text, 'UTF-8');

        if ($length < $minLength) {
            throw new \Exception("{$fieldName} must be at least {$minLength} characters");
        }

        if ($maxLength && $length > $maxLength) {
            throw new \Exception("{$fieldName} must not exceed {$maxLength} characters");
        }

        return true;
    }

    /**
     * Validate upload file
     */
    public static function validateUploadFile($file, $allowedTypes = [], $maxSize = 2097152)
    {
        if (!isset($file['tmp_name']) || empty($file['tmp_name'])) {
            throw new \Exception('Please select a file');
        }

        if ($file['error'] !== UPLOAD_ERR_OK) {
            throw new \Exception('File upload error occurred');
        }

        if ($file['size'] > $maxSize) {
            $maxSizeMB = $maxSize / 1024 / 1024;
            throw new \Exception("File size exceeds {$maxSizeMB} MB");
        }

        if (!empty($allowedTypes)) {
            $fileType = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
            if (!in_array($fileType, $allowedTypes)) {
                throw new \Exception('Invalid file type. Allowed: ' . implode(', ', $allowedTypes));
            }
        }

        return true;
    }
}

File Upload

1. Upload Controller

// modules/blog/controllers/upload.php
namespace Blog\Upload;

use Kotchasan\Http\Request;
use Kotchasan\File;

class Controller extends \Kotchasan\Controller
{
    /**
     * Upload Image
     */
    public function image(Request $request)
    {
        if ($request->initSession() && $request->isSafe()) {
            try {
                $file = $request->getUploadedFiles()['image'] ?? null;

                if (!$file) {
                    throw new \Exception('No file uploaded');
                }

                // Validate file
                \Blog\Validator\Model::validateUploadFile([
                    'tmp_name' => $file->getStream()->getMetadata('uri'),
                    'name' => $file->getClientFilename(),
                    'size' => $file->getSize(),
                    'error' => $file->getError()
                ], ['jpg', 'jpeg', 'png', 'gif'], 5242880); // 5MB

                // Generate new filename
                $extension = strtolower(pathinfo($file->getClientFilename(), PATHINFO_EXTENSION));
                $filename = uniqid() . '.' . $extension;
                $uploadPath = ROOT_PATH . '/datas/uploads/images/';

                // Create directory if not exists
                if (!is_dir($uploadPath)) {
                    mkdir($uploadPath, 0755, true);
                }

                // Move file
                $file->moveTo($uploadPath . $filename);

                // Resize image (optional)
                $this->resizeImage($uploadPath . $filename, 800, 600);

                return [
                    'alert' => 'Upload successful',
                    'filename' => $filename,
                    'url' => 'datas/uploads/images/' . $filename
                ];

            } catch (\Exception $e) {
                return [
                    'alert' => $e->getMessage()
                ];
            }
        }

        return [
            'alert' => 'Invalid upload request'
        ];
    }

    /**
     * Resize Image
     */
    private function resizeImage($imagePath, $maxWidth, $maxHeight)
    {
        $imageInfo = getimagesize($imagePath);
        if (!$imageInfo) {
            return false;
        }

        $originalWidth = $imageInfo[0];
        $originalHeight = $imageInfo[1];
        $imageType = $imageInfo[2];

        // Calculate new size
        $ratio = min($maxWidth / $originalWidth, $maxHeight / $originalHeight);
        if ($ratio >= 1) {
            return true; // No resize needed
        }

        $newWidth = intval($originalWidth * $ratio);
        $newHeight = intval($originalHeight * $ratio);

        // Create new image
        $newImage = imagecreatetruecolor($newWidth, $newHeight);

        switch ($imageType) {
            case IMAGETYPE_JPEG:
                $sourceImage = imagecreatefromjpeg($imagePath);
                break;
            case IMAGETYPE_PNG:
                $sourceImage = imagecreatefrompng($imagePath);
                imagealphablending($newImage, false);
                imagesavealpha($newImage, true);
                break;
            case IMAGETYPE_GIF:
                $sourceImage = imagecreatefromgif($imagePath);
                break;
            default:
                return false;
        }

        // Resize
        imagecopyresampled($newImage, $sourceImage, 0, 0, 0, 0, $newWidth, $newHeight, $originalWidth, $originalHeight);

        // Save image
        switch ($imageType) {
            case IMAGETYPE_JPEG:
                imagejpeg($newImage, $imagePath, 85);
                break;
            case IMAGETYPE_PNG:
                imagepng($newImage, $imagePath);
                break;
            case IMAGETYPE_GIF:
                imagegif($newImage, $imagePath);
                break;
        }

        // Free memory
        imagedestroy($sourceImage);
        imagedestroy($newImage);

        return true;
    }
}

2. Upload Form

// modules/blog/views/upload.php
namespace Blog\Upload;

use Kotchasan\Html;

class View extends \Gcms\View
{
    public function imageUploadForm()
    {
        $container = Html::create('div', ['class' => 'upload-container']);

        $container->add('h3', [], 'Upload Image');

        $form = $container->add('form', [
            'id' => 'upload_form',
            'class' => 'upload-form',
            'action' => 'index.php/blog/upload/image',
            'method' => 'POST',
            'enctype' => 'multipart/form-data',
            'onsubmit' => 'doFormSubmit',
            'ajax' => true,
            'token' => true
        ]);

        // File input
        $group = $form->add('div', ['class' => 'form-group']);
        $group->add('label', [], 'Select Image');
        $group->add('input', [
            'type' => 'file',
            'name' => 'image',
            'accept' => 'image/*',
            'required' => true
        ]);

        // Hint
        $group->add('small', ['class' => 'help-text'],
            'Supported files: JPG, PNG, GIF. Max size: 5MB');

        // Upload button
        $form->add('button', [
            'type' => 'submit',
            'class' => 'btn btn-primary'
        ], 'Upload');

        // Result area
        $container->add('div', [
            'id' => 'upload_result',
            'class' => 'upload-result'
        ]);

        return $container->render();
    }
}

Complete Web Page Examples

1. Dashboard Controller

// modules/blog/controllers/dashboard.php
namespace Blog\Dashboard;

use Gcms\Login;
use Kotchasan\Http\Request;

class Controller extends \Kotchasan\Controller
{
    public function render(Request $request)
    {
        // Check login
        $login = Login::isMember();
        if (!$login) {
            header('Location: index.php?module=blog&page=login');
            exit;
        }

        // Get stats
        $postModel = new \Blog\Post\Model();
        $userPosts = $postModel->getUserPosts($login['id']);
        $stats = $postModel->getUserStats($login['id']);

        $view = new View();
        return $view->render($login, $userPosts, $stats);
    }
}

2. Dashboard View

// modules/blog/views/dashboard.php
namespace Blog\Dashboard;

use Kotchasan\Html;

class View extends \Gcms\View
{
    public function render($user, $posts, $stats)
    {
        $container = Html::create('div', ['class' => 'dashboard-container']);

        // Header
        $header = $container->add('div', ['class' => 'dashboard-header']);
        $header->add('h1', [], 'Dashboard');
        $header->add('p', [], 'Welcome, ' . $user['name']);

        // Stats
        $statsRow = $container->add('div', ['class' => 'stats-row']);

        $statCard = $statsRow->add('div', ['class' => 'stat-card']);
        $statCard->add('h3', [], $stats['total_posts']);
        $statCard->add('p', [], 'Total Posts');

        $statCard = $statsRow->add('div', ['class' => 'stat-card']);
        $statCard->add('h3', [], $stats['published_posts']);
        $statCard->add('p', [], 'Published');

        $statCard = $statsRow->add('div', ['class' => 'stat-card']);
        $statCard->add('h3', [], number_format($stats['total_views']));
        $statCard->add('p', [], 'Total Views');

        // Quick Menu
        $quickMenu = $container->add('div', ['class' => 'quick-menu']);
        $quickMenu->add('h2', [], 'Quick Menu');

        $menuRow = $quickMenu->add('div', ['class' => 'menu-row']);
        $menuRow->add('a', [
            'href' => 'index.php?module=blog&page=post&action=form',
            'class' => 'menu-item'
        ], '+ Create New Post');

        $menuRow->add('a', [
            'href' => 'index.php?module=blog&page=post',
            'class' => 'menu-item'
        ], 'View All Posts');

        $menuRow->add('a', [
            'href' => 'index.php?module=blog&page=profile',
            'class' => 'menu-item'
        ], 'Edit Profile');

        // Recent Posts
        $recentPosts = $container->add('div', ['class' => 'recent-posts']);
        $recentPosts->add('h2', [], 'Your Recent Posts');

        if (empty($posts)) {
            $recentPosts->add('p', [], 'No posts found');
        } else {
            $table = $recentPosts->add('table', ['class' => 'data-table']);

            // Header
            $thead = $table->add('thead');
            $tr = $thead->add('tr');
            $tr->add('th', [], 'Title');
            $tr->add('th', [], 'Status');
            $tr->add('th', [], 'Views');
            $tr->add('th', [], 'Date');
            $tr->add('th', [], 'Action');

            // Body
            $tbody = $table->add('tbody');
            foreach ($posts as $post) {
                $tr = $tbody->add('tr');
                $tr->add('td', [], $post['title']);
                $tr->add('td', [], $post['status'] === 'published' ? 'Published' : 'Draft');
                $tr->add('td', [], number_format($post['views']));
                $tr->add('td', [], date('d/m/Y', strtotime($post['created_at'])));

                $actions = $tr->add('td');
                $actions->add('a', [
                    'href' => 'index.php?module=blog&page=post&action=form&id=' . $post['id'],
                    'class' => 'btn btn-sm btn-primary'
                ], 'Edit');

                $actions->add('a', [
                    'href' => 'index.php?module=blog&page=post&action=view&id=' . $post['id'],
                    'class' => 'btn btn-sm btn-secondary'
                ], 'View');
            }
        }

        return $container->render();
    }
}

Best Practices

1. Security

// Always use CSRF Token
$form = Html::create('form', [
    'token' => true, // Enable CSRF protection
    'ajax' => true
]);

// Check session and token
if ($request->initSession() && $request->isSafe()) {
    // Process data
}

// Escape data before output
echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');

// Use Prepared Statements
$users = $db->select('*')
    ->from('users')
    ->where('email', '=', $email) // Safe from SQL Injection
    ->execute();

2. Error Handling

try {
    // Code that might throw errors
    $result = $model->createPost($data);

    return [
        'alert' => 'Saved successfully',
        'location' => 'index.php?module=blog&page=dashboard'
    ];

} catch (\Exception $e) {
    // Log error for developers
    error_log('Post creation error: ' . $e->getMessage());

    // Show appropriate message to users
    return [
        'alert' => 'Error occurred: ' . $e->getMessage(),
        'input' => 'title' // Focus on problematic field
    ];
}

3. Data Validation

class ExampleClass {
    <?php
// Validate on both Client and Server side
// Client-side (JavaScript)
document.getElementById('email').addEventListener('blur', function() {
    const email = this.value;
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

    if (!emailRegex.test(email)) {
        this.setCustomValidity('Invalid email format');
    } else {
        this.setCustomValidity('');
    }
});

// Server-side (PHP)
public function validateEmail($email)
{
    if (empty($email)) {
        throw new \Exception('Email is required');
    }

    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        throw new \Exception('Invalid email format');
    }

    return true;
}
}

4. Session Management

// Check login
function requireLogin()
{
    if (!Session::get('user_id')) {
        header('Location: index.php?module=blog&page=login');
        exit;
    }

    return [
        'id' => Session::get('user_id'),
        'name' => Session::get('user_name'),
        'role' => Session::get('user_role')
    ];
}

// Usage
$user = requireLogin();

5. Performance Optimization

// Use Pagination
public function getAllPosts($page = 1, $limit = 10)
{
    $offset = ($page - 1)  $limit;

    return $this->db->select()
        ->from('posts')
        ->where('status', '=', 'published')
        ->orderBy('created_at', 'DESC')
        ->limit($limit, $offset)
        ->fetchAll();
}

// Use Cache for infrequently changing data
public function getCategories()
{
    $cacheKey = 'blog_categories';
    $categories = Cache::get($cacheKey);

    if ($categories === null) {
        $categories = $this->db->select('*')
            ->from('categories')
            ->orderBy('name')
            ->execute();

        Cache::set($cacheKey, $categories, 3600); // cache for 1 hour
    }

    return $categories;
}

// Lazy Loading for images
<img src="placeholder.jpg" data-src="actual-image.jpg" class="lazy-load" alt="...">
}

6. Code Organization

// Separate Business Logic from Controller
class PostController extends \Kotchasan\Controller
{
    private $postService;

    public function __construct()
    {
        $this->postService = new PostService();
    }

    public function create(Request $request)
    {
        try {
            $data = $request->getParsedBody();
            $post = $this->postService->createPost($data);

            return $this->successResponse($post);
        } catch (\Exception $e) {
            return $this->errorResponse($e->getMessage());
        }
    }
}

// Service Class
class PostService
{
    private $postModel;
    private $validator;

    public function __construct()
    {
        $this->postModel = new PostModel();
        $this->validator = new PostValidator();
    }

    public function createPost($data)
    {
        $this->validator->validate($data);
        return $this->postModel->create($data);
    }
}

Summary

Web development with Kotchasan Framework emphasizes:

  1. Security - Use CSRF Token, Prepared Statements, Input Validation
  2. Organization - Use MVC Pattern, separate Business Logic
  3. Performance - Use Cache, Pagination, Optimization
  4. Convenience - Form Helper, Validation Helper, Upload Helper
  5. Maintainability - Error Handling, Logging, Testing

Following these Best Practices will help create high-quality, secure, and efficient web pages.