Kotchasan Framework Documentation
Kotchasan MVC Architecture Guide
Kotchasan MVC Architecture Guide
Introduction
The Kotchasan Framework uses the MVC (Model-View-Controller) architectural pattern that helps separate application functionality into 3 main parts, making code well-organized, easy to maintain, and scalable.
Table of Contents
- MVC Pattern Overview
- Directory Structure
- Model Layer
- View Layer
- Controller Layer
- MVC Collaboration
- Module Creation Examples
- Best Practices
MVC Pattern Overview
Model
- Manages data and business logic
- Connects to the database
- Validates data integrity
- Processes data before sending to Controller
View
- Manages presentation and UI
- Creates HTML, JSON, or other output formats
- Receives data from Controller and renders output
- Contains no business logic
Controller
- Manages HTTP Request and Response
- Connects Model and View
- Decides which Model and View to use
- Handles routing and authentication
Directory Structure
modules/
└── mymodule/
├── controllers/
│ ├── index.php # Main Controller (namespace MyModule\Index)
│ ├── api.php # API Controller (namespace MyModule\Api)
│ ├── user.php # User Controller (namespace MyModule\User)
│ └── admin.php # Admin Controller (namespace MyModule\Admin)
├── models/
│ ├── index.php # Index Model (namespace MyModule\Index)
│ ├── user.php # User Model (namespace MyModule\User)
│ └── validation.php # Validation Model (namespace MyModule\Validation)
├── views/
│ ├── index.php # Main View (namespace MyModule\Index)
│ ├── user.php # User View (namespace MyModule\User)
│ └── admin.php # Admin View (namespace MyModule\Admin)
├── template/
│ └── email.html # Email template
├── script.js # JavaScript
└── style.css # CSSModel Layer
Creating a Model
namespace MyModule\User;
use Kotchasan\Model;
use Kotchasan\Database;
/**
* User Model
* File: modules/mymodule/models/user.php
*/
class Model extends \Kotchasan\Model
{
/**
* Table name
*/
protected $table = 'users';
/**
* Kotchasan\Database connection (defaults to 'default' if not specified)
*/
protected $conn = 'default';
/**
* Get all users
*/
public function getAllUsers()
{
return $this->db->select()
->from($this->table)
->where('status', '=', 'active')
->orderBy('created_at', 'DESC')
->fetchAll();
}
/**
* Get user by ID
*/
public function getUserById($id)
{
return $this->db->select()
->from($this->table)
->where('id', '=', $id)
->first();
}
/**
* Create new user
*/
public function createUser($data)
{
// Validate data
$this->validateUserData($data);
// Hash password
$data['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
$data['created_at'] = date('Y-m-d H:i:s');
$this->db->insert($this->table)
->values($data)
->execute();
return $this->db->lastInsertId();
}
/**
* Update user
*/
public function updateUser($id, $data)
{
$data['updated_at'] = date('Y-m-d H:i:s');
return $this->db->update($this->table)
->set($data)
->where('id', '=', $id)
->execute();
}
/**
* Delete user
*/
public function deleteUser($id)
{
return $this->db->delete($this->table)
->where('id', '=', $id)
->execute();
}
/**
* Validate user data
*/
private function validateUserData($data)
{
if (empty($data['name'])) {
throw new \Exception('Name is required');
}
if (empty($data['email']) || !filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
throw new \Exception('Valid email is required');
}
// 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');
}
}
}Advanced Model Usage
namespace MyModule\Post;
use Kotchasan\Model;
class Model extends \Kotchasan\Model
{
/**
* Get posts with author information
*/
public function getPostsWithAuthor($limit = 10)
{
return $this->db->select('p.', 'u.name as author_name', 'u.avatar')
->from('posts p')
->leftJoin('users u', 'u.id', 'p.user_id')
->where('p.status', '=', 'published')
->orderBy('p.created_at', 'DESC')
->limit($limit)
->fetchAll();
}
/**
* Search posts
*/
public function searchPosts($keyword, $category = null)
{
$query = $this->db->select()
->from('posts')
->where('status', '=', 'published');
if (!empty($keyword)) {
$query->where('title', 'LIKE', "%{$keyword}%")
->orWhere('content', 'LIKE', "%{$keyword}%");
}
if ($category) {
$query->where('category_id', '=', $category);
}
return $query->orderBy('created_at', 'DESC')->fetchAll();
}
/**
* Get post statistics
*/
public function getPostStats()
{
return [
'total' => $this->db->select('COUNT() as count')
->from('posts')
->first()['count'],
'published' => $this->db->select('COUNT() as count')
->from('posts')
->where('status', '=', 'published')
->first()['count'],
'draft' => $this->db->select('COUNT() as count')
->from('posts')
->where('status', '=', 'draft')
->first()['count']
];
}
}View Layer
Creating a View
namespace MyModule\User;
use Kotchasan\Html;
use Kotchasan\Http\Request;
use Kotchasan\Language;
/**
* User View
* File: modules/mymodule/views/user.php
*/
class View extends \Gcms\View
{
/**
* Display user list
*/
public function render(Request $request)
{
// Get data from Model
$model = new \MyModule\User\Model();
$users = $model->getAllUsers();
// Create HTML Table
$table = Html::create('table', [
'class' => 'data fullwidth'
]);
// Header
$thead = $table->add('thead');
$tr = $thead->add('tr');
$tr->add('th', [], 'ID');
$tr->add('th', [], 'Name');
$tr->add('th', [], 'Email');
$tr->add('th', [], 'Status');
$tr->add('th', [], 'Actions');
// Body
$tbody = $table->add('tbody');
foreach ($users as $user) {
$tr = $tbody->add('tr');
$tr->add('td', [], $user['id']);
$tr->add('td', [], $user['name']);
$tr->add('td', [], $user['email']);
$tr->add('td', [], $user['status']);
// Action buttons
$actions = $tr->add('td');
$actions->add('a', [
'href' => 'index.php?module=mymodule&page=user&action=edit&id=' . $user['id'],
'class' => 'button green'
], 'Edit');
$actions->add('a', [
'href' => 'index.php?module=mymodule&page=user&action=delete&id=' . $user['id'],
'class' => 'button red',
'onclick' => 'return confirm("Are you sure you want to delete?")'
], 'Delete');
}
return $table->render();
}
/**
* Add/Edit user form
*/
public function form(Request $request)
{
$id = $request->request('id')->toInt();
$user = [];
if ($id > 0) {
$model = new \MyModule\User\Model();
$user = $model->getUserById($id);
}
// Create form
$form = Html::create('form', [
'id' => 'user_form',
'class' => 'setup_frm',
'action' => 'index.php/mymodule/model/user/save',
'onsubmit' => 'doFormSubmit',
'ajax' => true,
'token' => true
]);
// Hidden ID field
if ($id > 0) {
$form->add('input', [
'type' => 'hidden',
'name' => 'id',
'value' => $id
]);
}
// Name field
$fieldset = $form->add('fieldset');
$fieldset->add('legend', [], 'User Information');
$groups = $fieldset->add('div', ['class' => 'item']);
$groups->add('label', [], 'Name');
$groups->add('input', [
'type' => 'text',
'name' => 'name',
'value' => isset($user['name']) ? $user['name'] : '',
'required' => true
]);
// Email field
$groups = $fieldset->add('div', ['class' => 'item']);
$groups->add('label', [], 'Email');
$groups->add('input', [
'type' => 'email',
'name' => 'email',
'value' => isset($user['email']) ? $user['email'] : '',
'required' => true
]);
// Password field (only for new users)
if ($id == 0) {
$groups = $fieldset->add('div', ['class' => 'item']);
$groups->add('label', [], 'Password');
$groups->add('input', [
'type' => 'password',
'name' => 'password',
'required' => true
]);
}
// Save button
$fieldset = $form->add('fieldset');
$fieldset->add('input', [
'class' => 'button save large',
'type' => 'submit',
'value' => 'Save'
]);
return $form->render();
}
}Creating JSON Response
namespace MyModule\Api;
use Kotchasan\Http\Request;
use Kotchasan\Http\Response;
class View extends \Kotchasan\View
{
/**
* Send JSON data
*/
public function json(Request $request)
{
$model = new \MyModule\User\Model();
$users = $model->getAllUsers();
// Create JSON Response
$response = new Response();
$response = $response->withHeader('Content-Type', 'application/json');
$data = [
'success' => true,
'data' => $users,
'total' => count($users)
];
$response->getBody()->write(json_encode($data, JSON_UNESCAPED_UNICODE));
return $response;
}
}Controller Layer
Creating a Controller
namespace MyModule\User;
use Gcms\Login;
use Kotchasan\Http\Request;
use Kotchasan\Language;
/**
* User Controller
* File: modules/mymodule/controllers/user.php
*/
class Controller extends \Kotchasan\Controller
{
/**
* Display user list
*/
public function render(Request $request)
{
// Check permissions
$login = Login::isMember();
if (!$login || !Login::checkPermission($login, 'can_manage_user')) {
return new \Kotchasan\Http\NotFound();
}
// Call View
$view = new \MyModule\User\View();
return $view->render($request);
}
/**
* Display form
*/
public function form(Request $request)
{
$login = Login::isMember();
if (!$login || !Login::checkPermission($login, 'can_manage_user')) {
return new \Kotchasan\Http\NotFound();
}
$view = new \MyModule\User\View();
return $view->form($request);
}
}API Controller
namespace MyModule\Api;
use Kotchasan\ApiController;
use Kotchasan\Http\Request;
/**
* API Controller
* File: modules/mymodule/controllers/api.php
*/
class Controller extends ApiController
{
/**
* GET /api/users
*/
public function index(Request $request)
{
try {
$model = new \MyModule\User\Model();
$users = $model->getAllUsers();
return $this->successResponse($users, 'Data retrieved successfully');
} catch (\Exception $e) {
return $this->errorResponse($e->getMessage(), 500);
}
}
/**
* POST /api/users
*/
public function store(Request $request)
{
try {
$data = [
'name' => $request->post('name')->toString(),
'email' => $request->post('email')->toString(),
'password' => $request->post('password')->toString()
];
$model = new \MyModule\User\Model();
$userId = $model->createUser($data);
return $this->successResponse(['id' => $userId], 'User created successfully', 201);
} catch (\Exception $e) {
return $this->errorResponse($e->getMessage(), 400);
}
}
/**
* PUT /api/users/{id}
*/
public function update(Request $request)
{
try {
$id = $request->request('id')->toInt();
$data = [
'name' => $request->post('name')->toString(),
'email' => $request->post('email')->toString()
];
$model = new \MyModule\User\Model();
$model->updateUser($id, $data);
return $this->successResponse([], 'Updated successfully');
} catch (\Exception $e) {
return $this->errorResponse($e->getMessage(), 400);
}
}
/**
* DELETE /api/users/{id}
*/
public function destroy(Request $request)
{
try {
$id = $request->request('id')->toInt();
$model = new \MyModule\User\Model();
$model->deleteUser($id);
return $this->successResponse([], 'Deleted successfully');
} catch (\Exception $e) {
return $this->errorResponse($e->getMessage(), 400);
}
}
}MVC Collaboration
Workflow
- Request comes to Router
- Router calls appropriate Controller
- Controller checks permissions and calls Model
- Model processes data and returns results
- Controller sends data to View
- View creates HTML/JSON and returns
- Response is sent back to Client
Complete Workflow Example
// 1. Router (index.php)
$request = \Kotchasan\Http\Request::createFromGlobals();
$router = new \Kotchasan\Http\Router();
$router->get('/users', '\MyModule\User\Controller@render');
$router->post('/users/save', '\MyModule\User\Model@save');
// 2. Controller receives Request
class Controller extends \Kotchasan\Controller
{
public function render(Request $request)
{
// 3. Call Model
$model = new Model();
$users = $model->getAllUsers();
// 4. Send data to View
$view = new View();
return $view->render($users);
}
}
// 5. Model processes data
class Model extends \Kotchasan\Model
{
public function getAllUsers()
{
return $this->db->select('*')
->from('users')
->where('status', '=', 'active')
->fetchAll();
}
}
// 6. View creates HTML
class View extends \Kotchasan\View
{
public function render($users)
{
$html = '<table>';
foreach ($users as $user) {
$html .= '<tr><td>' . $user['name'] . '</td></tr>';
}
$html .= '</table>';
return $html;
}
}Module Creation Examples
1. Create Directory Structure
mkdir -p modules/blog/{controllers,models,views,template}2. Create Model
// modules/blog/models/post.php
namespace Blog\Post;
use Kotchasan\Model;
class Model extends \Kotchasan\Model
{
protected $table = 'blog_posts';
/**
* Retrieve posts with author information
*/
public function getPostsWithAuthor($limit = 10, $offset = 0)
{
return $this->db->select('p.', 'u.name as author')
->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();
}
/**
* Retrieve post by ID
*/
public function getPostById($id)
{
return $this->db->select()
->from($this->table)
->where('id', '=', $id)
->where('status', '=', 'published')
->first();
}
/**
* Create new post
*/
public function createPost($data)
{
$data['created_at'] = date('Y-m-d H:i:s');
$data['status'] = 'draft';
$this->db->insert($this->table)
->values($data)
->execute();
return $this->db->lastInsertId();
}
}3. Create View
// modules/blog/views/index.php
namespace Blog\Index;
use Kotchasan\Html;
use Kotchasan\Http\Request;
class View extends \Gcms\View
{
public function render(Request $request)
{
$model = new \Blog\Post\Model();
$posts = $model->getAllPosts();
$container = Html::create('div', ['class' => 'blog-container']);
foreach ($posts as $post) {
$article = $container->add('article', ['class' => 'blog-post']);
$article->add('h2', [], $post['title']);
$article->add('p', ['class' => 'meta'],
'By ' . $post['author'] . ' on ' . $post['created_at']
);
$article->add('div', ['class' => 'content'],
substr($post['content'], 0, 200) . '...'
);
$article->add('a', [
'href' => 'index.php?module=blog&page=post&id=' . $post['id'],
'class' => 'read-more'
], 'Read More');
}
return $container->render();
}
}4. Create Controller
// modules/blog/controllers/index.php
namespace Blog\Index;
use Kotchasan\Http\Request;
class Controller extends \Kotchasan\Controller
{
public function render(Request $request)
{
$view = new View();
return $view->render($request);
}
}Best Practices
1. Naming Conventions
// Good - meaningful names
class UserController extends \Kotchasan\Controller
{
public function getUserProfile(Request $request) { }
public function updateUserSettings(Request $request) { }
}
// Bad - unclear names
class Controller extends \Kotchasan\Controller
{
public function action1(Request $request) { }
public function doSomething(Request $request) { }
}2. Separation of Concerns
// Model - handles data only
class UserModel extends \Kotchasan\Model
{
public function findUserByEmail($email)
{
return $this->db->select('*')
->from('users')
->where('email', '=', $email)
->first();
}
}
// Controller - handles logic and flow
class UserController extends \Kotchasan\Controller
{
public function login(Request $request)
{
$email = $request->post('email')->toString();
$password = $request->post('password')->toString();
$model = new UserModel();
$user = $model->findUserByEmail($email);
if ($user && password_verify($password, $user['password'])) {
// Login successful
$view = new DashboardView();
return $view->render($user);
} else {
// Login failed
$view = new LoginView();
return $view->renderWithError('Invalid email or password');
}
}
}
// View - handles presentation only
class LoginView extends \Kotchasan\View
{
public function renderWithError($message)
{
$form = Html::create('form');
$form->add('div', ['class' => 'error'], $message);
// ... create login form
return $form->render();
}
}3. Error Handling
class UserController extends \Kotchasan\Controller
{
public function createUser(Request $request)
{
try {
$model = new UserModel();
$userId = $model->createUser($request->getParsedBody());
return $this->successResponse(['id' => $userId]);
} catch (\InvalidArgumentException $e) {
return $this->errorResponse($e->getMessage(), 400);
} catch (\Exception $e) {
error_log($e->getMessage());
return $this->errorResponse('System error occurred', 500);
}
}
}4. Dependency Injection
class UserController extends \Kotchasan\Controller
{
private $userModel;
private $emailService;
public function __construct(UserModel $userModel, EmailService $emailService)
{
$this->userModel = $userModel;
$this->emailService = $emailService;
}
public function registerUser(Request $request)
{
$userData = $request->getParsedBody();
$userId = $this->userModel->createUser($userData);
// Send welcome email
$this->emailService->sendWelcomeEmail($userData['email']);
return $this->successResponse(['id' => $userId]);
}
}5. Testing
// Model testing
class UserModelTest extends \PHPUnit\Framework\TestCase
{
public function testCreateUser()
{
$model = new UserModel();
$userData = [
'name' => 'Test User',
'email' => 'test@example.com',
'password' => 'password123'
];
$userId = $model->createUser($userData);
$this->assertGreaterThan(0, $userId);
$user = $model->getUserById($userId);
$this->assertEquals('Test User', $user['name']);
}
}
// Controller testing
class UserControllerTest extends \PHPUnit\Framework\TestCase
{
public function testLoginSuccess()
{
$request = $this->createMockRequest([
'email' => 'test@example.com',
'password' => 'password123'
]);
$controller = new UserController();
$response = $controller->login($request);
$this->assertEquals(200, $response->getStatusCode());
}
}Summary
Using the MVC pattern in Kotchasan Framework provides:
- Well-organized code - Clear separation of concerns
- Easy maintenance - Modify one part without affecting others
- Scalability - Easy to add new features
- Testability - Each part can be tested separately
- Team collaboration - Multiple developers can work simultaneously
Following MVC Pattern and Best Practices will help create high-quality, sustainable applications.