Kotchasan Framework Documentation
Controller Classes
Controller Classes
Overview
Controller Classes in the Kotchasan Framework are a core component of the MVC pattern, acting as an intermediary between the Model and View. They handle HTTP requests, business logic, and passing data to the View.
Key Features
- MVC Pattern: Follows the Model-View-Controller pattern.
- Request Handling: Handles all types of HTTP requests.
- API Support: Supports creating REST APIs.
- Authentication: Authentication and authorization system.
- Validation: Data validation.
- Error Handling: Consistent error handling.
- Response Formatting: Formatting responses.
Table of Contents
- Controller Base Class
- ApiController - REST API Controller
- HTTP Request and Response Handling
- Authentication and Security
- Error Handling and Validation
- Best Practices
Controller Base Class
The Controller base class is the foundation for creating all controllers.
Basic Controller Usage
use Kotchasan\Database;
use Kotchasan\Controller;
use Kotchasan\Http\Request;
class UserController extends Controller
{
/**
* Display user list
*/
public function index(Request $request)
{
// Get user data
$users = Kotchasan\Database::create()
->select(['id', 'name', 'email', 'status'])
->from('users')
->where(['status', 'active'])
->orderBy('name')
->fetchAll();
// Pass data to View
return View::create()
->assign('users', $users)
->render('user/index');
}
/**
* Display create user form
*/
public function create(Request $request)
{
return View::create()->render('user/create');
}
/**
* Save new user
*/
public function store(Request $request)
{
$input = $request->getParsedBody();
// Validation
$errors = $this->validateUserInput($input);
if (!empty($errors)) {
return View::create()
->assign('errors', $errors)
->assign('input', $input)
->render('user/create');
}
// Save data
$userId = Kotchasan\Database::create()
->insert('users')
->values([
'name' => $input['name'],
'email' => $input['email'],
'password' => password_hash($input['password'], PASSWORD_DEFAULT),
'created_at' => date('Y-m-d H:i:s')
])
->execute();
// Redirect after successful save
return Response::create()
->withStatus(302)
->withHeader('Location', '/users/' . $userId);
}
/**
* Display user data
*/
public function show(Request $request)
{
$userId = $request->getAttribute('id');
$user = Kotchasan\Database::create()
->select(['id', 'name', 'email', 'created_at'])
->from('users')
->where(['id', $userId])
->first();
if (!$user) {
return Response::create()
->withStatus(404)
->getBody()->write('User not found');
}
return View::create()
->assign('user', $user)
->render('user/show');
}
/**
* Validation for user data
*/
private function validateUserInput($input)
{
$errors = [];
if (empty($input['name'])) {
$errors['name'] = 'Name is required';
}
if (empty($input['email'])) {
$errors['email'] = 'Email is required';
} elseif (!filter_var($input['email'], FILTER_VALIDATE_EMAIL)) {
$errors['email'] = 'Invalid email format';
}
if (empty($input['password'])) {
$errors['password'] = 'Password is required';
} elseif (strlen($input['password']) < 6) {
$errors['password'] = 'Password must be at least 6 characters';
}
return $errors;
}
}Factory Pattern
// Creating a Controller instance
$controller = Controller::create();
// Usage in routing
$userController = UserController::create();
$response = $userController->index($request);ApiController - REST API Controller
ApiController is a specialized class for creating REST API endpoints.
ApiController Features
- REST API Support: Supports REST API patterns.
- JSON Response: Sends results as JSON.
- Authentication: Token-based authentication system.
- CORS Support: Supports Cross-Origin Resource Sharing.
- Rate Limiting: Limits API call rates.
- Error Standardization: Standardized error responses.
Using ApiController
use Kotchasan\Database;
use Kotchasan\ApiController;
use Kotchasan\Http\Request;
use Kotchasan\Http\Response;
class UserApiController extends ApiController
{
/**
* Get user list - GET /api/users
*/
public function index(Request $request): Response
{
try {
// Check authentication
$this->requireAuthentication($request);
// Get parameters from query string
$page = (int) $request->getQueryParam('page', 1);
$limit = (int) $request->getQueryParam('limit', 20);
$search = $request->getQueryParam('search', '');
// Create query
$query = Kotchasan\Database::create()
->select(['id', 'name', 'email', 'status', 'created_at'])
->from('users');
// Add search conditions
if (!empty($search)) {
$query->where(function($q) use ($search) {
$q->where(['name', 'LIKE', "%{$search}%"])
->orWhere(['email', 'LIKE', "%{$search}%"]);
});
}
// Count total
$total = $query->count();
// Get data with pagination
$users = $query
->orderBy('created_at', 'DESC')
->limit($limit, ($page - 1) * $limit)
->fetchAll();
// Send paginated response
return $this->paginatedResponse($users, $total, $page, $limit);
} catch (Exception $e) {
return $this->errorResponse(
'Error retrieving user data',
500,
['error' => $e->getMessage()]
);
}
}
/**
* Get specific user - GET /api/users/{id}
*/
public function show(Request $request): Response
{
try {
$this->requireAuthentication($request);
$userId = $request->getAttribute('id');
$user = Kotchasan\Database::create()
->select(['id', 'name', 'email', 'status', 'created_at', 'updated_at'])
->from('users')
->where(['id', $userId])
->first();
if (!$user) {
return $this->errorResponse('User not found', 404);
}
return $this->successResponse($user, 'User retrieved successfully');
} catch (Exception $e) {
return $this->errorResponse(
'Error retrieving user data',
500,
['error' => $e->getMessage()]
);
}
}
/**
* Create new user - POST /api/users
*/
public function store(Request $request): Response
{
try {
$this->requireAuthentication($request);
$input = $request->getParsedBody();
// Validation
$validationErrors = $this->validateInput([
'name' => 'required|string|min:2|max:100',
'email' => 'required|email|unique:users,email',
'password' => 'required|string|min:6'
], $input);
if (!empty($validationErrors)) {
return $this->errorResponse(
'Invalid data',
422,
$validationErrors
);
}
// Create new user
$userId = Kotchasan\Database::create()
->insert('users')
->values([
'name' => $input['name'],
'email' => $input['email'],
'password' => password_hash($input['password'], PASSWORD_DEFAULT),
'status' => 'active',
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s')
])
->execute();
// Get newly created user
$newUser = Kotchasan\Database::create()
->select(['id', 'name', 'email', 'status', 'created_at'])
->from('users')
->where(['id', $userId])
->first();
return $this->successResponse(
$newUser,
'User created successfully',
201
);
} catch (Exception $e) {
return $this->errorResponse(
'Error creating user',
500,
['error' => $e->getMessage()]
);
}
}
/**
* Update user data - PUT/PATCH /api/users/{id}
*/
public function update(Request $request): Response
{
try {
$this->requireAuthentication($request);
$userId = $request->getAttribute('id');
$input = $request->getParsedBody();
// Check if user exists
$existingUser = Kotchasan\Database::create()
->select(['id'])
->from('users')
->where(['id', $userId])
->first();
if (!$existingUser) {
return $this->errorResponse('User not found', 404);
}
// Validation
$validationErrors = $this->validateInput([
'name' => 'string|min:2|max:100',
'email' => "email|unique:users,email,{$userId}",
'password' => 'string|min:6'
], $input);
if (!empty($validationErrors)) {
return $this->errorResponse(
'Invalid data',
422,
$validationErrors
);
}
// Prepare update data
$updateData = ['updated_at' => date('Y-m-d H:i:s')];
if (isset($input['name'])) {
$updateData['name'] = $input['name'];
}
if (isset($input['email'])) {
$updateData['email'] = $input['email'];
}
if (isset($input['password'])) {
$updateData['password'] = password_hash($input['password'], PASSWORD_DEFAULT);
}
// Update data
Kotchasan\Database::create()
->update('users')
->set($updateData)
->where(['id', $userId])
->execute();
// Get updated user
$updatedUser = Kotchasan\Database::create()
->select(['id', 'name', 'email', 'status', 'updated_at'])
->from('users')
->where(['id', $userId])
->first();
return $this->successResponse(
$updatedUser,
'User updated successfully'
);
} catch (Exception $e) {
return $this->errorResponse(
'Error updating user',
500,
['error' => $e->getMessage()]
);
}
}
/**
* Delete user - DELETE /api/users/{id}
*/
public function destroy(Request $request): Response
{
try {
$this->requireAuthentication($request);
$userId = $request->getAttribute('id');
// Check if user exists
$existingUser = Kotchasan\Database::create()
->select(['id', 'name'])
->from('users')
->where(['id', $userId])
->first();
if (!$existingUser) {
return $this->errorResponse('User not found', 404);
}
// Delete user
Kotchasan\Database::create()
->delete('users')
->where(['id', $userId])
->execute();
return $this->successResponse(
['deleted_user' => $existingUser],
'User deleted successfully'
);
} catch (Exception $e) {
return $this->errorResponse(
'Error deleting user',
500,
['error' => $e->getMessage()]
);
}
}
}HTTP Request and Response Handling
Request Handling
use Kotchasan\Database;
class ProductController extends Controller
{
public function search(Request $request)
{
// Get data from Query Parameters
$query = $request->getQueryParam('q', '');
$category = $request->getQueryParam('category', '');
$minPrice = (float) $request->getQueryParam('min_price', 0);
$maxPrice = (float) $request->getQueryParam('max_price', 0);
$page = (int) $request->getQueryParam('page', 1);
$limit = (int) $request->getQueryParam('limit', 20);
// Get data from Request Body (POST/PUT)
$postData = $request->getParsedBody();
// Get data from Headers
$authToken = $request->getHeaderLine('Authorization');
$contentType = $request->getHeaderLine('Content-Type');
// Get data from Cookies
$sessionId = $request->getCookieParam('session_id');
// Get data from Route Parameters
$productId = $request->getAttribute('id');
// Get uploaded files
$uploadedFiles = $request->getUploadedFiles();
// Create search query
$searchQuery = Kotchasan\Database::create()
->select(['id', 'name', 'price', 'category', 'image'])
->from('products');
// Add search conditions
if (!empty($query)) {
$searchQuery->where(['name', 'LIKE', "%{$query}%"]);
}
if (!empty($category)) {
$searchQuery->where(['category', $category]);
}
if ($minPrice > 0) {
$searchQuery->where(['price', '>=', $minPrice]);
}
if ($maxPrice > 0) {
$searchQuery->where(['price', '<=', $maxPrice]);
}
// Count total results
$total = $searchQuery->count();
// Get data with pagination
$products = $searchQuery
->orderBy('name')
->limit($limit, ($page - 1) * $limit)
->fetchAll();
return View::create()
->assign('products', $products)
->assign('total', $total)
->assign('currentPage', $page)
->assign('totalPages', ceil($total / $limit))
->render('product/search');
}
}Response Handling
class ResponseController extends Controller
{
/**
* Send JSON Response
*/
public function jsonResponse()
{
$data = [
'status' => 'success',
'message' => 'Data retrieved successfully',
'data' => [
'users' => [
['id' => 1, 'name' => 'John'],
['id' => 2, 'name' => 'Jane']
]
],
'timestamp' => time()
];
return Response::create()
->withHeader('Content-Type', 'application/json')
->withStatus(200)
->getBody()->write(json_encode($data));
}
/**
* Send HTML Response
*/
public function htmlResponse()
{
$content = View::create()
->assign('title', 'Homepage')
->assign('message', 'Welcome')
->render('home/index');
return Response::create()
->withHeader('Content-Type', 'text/html; charset=utf-8')
->withStatus(200)
->getBody()->write($content);
}
/**
* Redirect Response
*/
public function redirectResponse()
{
return Response::create()
->withStatus(302)
->withHeader('Location', '/dashboard');
}
/**
* File Download Response
*/
public function downloadFile(Request $request)
{
$filename = $request->getAttribute('filename');
$filePath = '/path/to/files/' . $filename;
if (!file_exists($filePath)) {
return Response::create()
->withStatus(404)
->getBody()->write('File not found');
}
$fileContent = file_get_contents($filePath);
return Response::create()
->withHeader('Content-Type', 'application/octet-stream')
->withHeader('Content-Disposition', 'attachment; filename="' . $filename . '"')
->withHeader('Content-Length', strlen($fileContent))
->withStatus(200)
->getBody()->write($fileContent);
}
/**
* Image Response
*/
public function serveImage(Request $request)
{
$imageId = $request->getAttribute('id');
$imagePath = '/path/to/images/' . $imageId . '.jpg';
if (!file_exists($imagePath)) {
return Response::create()->withStatus(404);
}
$imageContent = file_get_contents($imagePath);
return Response::create()
->withHeader('Content-Type', 'image/jpeg')
->withHeader('Cache-Control', 'public, max-age=86400')
->withStatus(200)
->getBody()->write($imageContent);
}
}Authentication and Security
Token-based Authentication
use Kotchasan\Database;
class AuthController extends Controller
{
/**
* Login and Create Token
*/
public function login(Request $request): Response
{
$input = $request->getParsedBody();
// Validation
if (empty($input['email']) || empty($input['password'])) {
return $this->errorResponse('Please enter email and password', 400);
}
// Check user
$user = Kotchasan\Database::create()
->select(['id', 'name', 'email', 'password', 'status'])
->from('users')
->where(['email', $input['email']])
->first();
if (!$user || !password_verify($input['password'], $user['password'])) {
return $this->errorResponse('Invalid email or password', 401);
}
if ($user['status'] !== 'active') {
return $this->errorResponse('User account disabled', 403);
}
// Create JWT Token
$payload = [
'user_id' => $user['id'],
'email' => $user['email'],
'iat' => time(),
'exp' => time() + (24 * 60 * 60) // 24 hours
];
$token = $this->generateJWTToken($payload);
// Update last login
Kotchasan\Database::create()
->update('users')
->set(['last_login' => date('Y-m-d H:i:s')])
->where(['id', $user['id']])
->execute();
return $this->successResponse([
'user' => [
'id' => $user['id'],
'name' => $user['name'],
'email' => $user['email']
],
'token' => $token,
'expires_in' => 86400
], 'Login successful');
}
/**
* Check Token
*/
protected function requireAuthentication(Request $request)
{
$authHeader = $request->getHeaderLine('Authorization');
if (empty($authHeader)) {
throw new AuthenticationException('Missing Authorization header', 401);
}
// Extract Token from Authorization header
if (!preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
throw new AuthenticationException('Invalid Authorization header format', 401);
}
$token = $matches[1];
// Verify JWT Token
try {
$payload = $this->verifyJWTToken($token);
// Check if token is not expired
if ($payload['exp'] < time()) {
throw new AuthenticationException('Token expired', 401);
}
// Get user data
$user = Kotchasan\Database::create()
->select(['id', 'name', 'email', 'status'])
->from('users')
->where(['id', $payload['user_id']])
->first();
if (!$user || $user['status'] !== 'active') {
throw new AuthenticationException('Invalid user', 401);
}
// Store user data in request attribute
$request = $request->withAttribute('user', $user);
return $user;
} catch (Exception $e) {
throw new AuthenticationException('Invalid token: ' . $e->getMessage(), 401);
}
}
/**
* Logout
*/
public function logout(Request $request): Response
{
// In JWT token-based auth, logout is done by removing the token on the client
// or keeping a blacklist of logged out tokens
return $this->successResponse(
['message' => 'Logged out successfully'],
'Logout successful'
);
}
/**
* Get current user profile
*/
public function profile(Request $request): Response
{
$user = $this->requireAuthentication($request);
return $this->successResponse($user, 'Profile retrieved successfully');
}
}Error Handling and Validation
Centralized Error Handling
class ErrorController extends Controller
{
/**
* Handle 404 Error
*/
public function notFound(Request $request): Response
{
$isApiRequest = strpos($request->getHeaderLine('Content-Type'), 'application/json') !== false
|| strpos($request->getUri()->getPath(), '/api/') === 0;
if ($isApiRequest) {
return $this->errorResponse('Endpoint not found', 404);
}
return View::create()
->assign('title', 'Page Not Found')
->assign('message', 'Sorry, the page you are looking for does not exist')
->render('errors/404')
->withStatus(404);
}
/**
* Handle 500 Error
*/
public function serverError(Request $request, Exception $exception): Response
{
// Log error
error_log("Server Error: " . $exception->getMessage() . "\n" . $exception->getTraceAsString());
$isApiRequest = strpos($request->getHeaderLine('Content-Type'), 'application/json') !== false
|| strpos($request->getUri()->getPath(), '/api/') === 0;
if ($isApiRequest) {
$errorData = [
'error' => 'Internal server error'
];
// Show error details in development mode
if (self::$cfg->debug) {
$errorData['debug'] = [
'message' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => $exception->getTrace()
];
}
return $this->errorResponse(
'Internal server error',
500,
$errorData
);
}
return View::create()
->assign('title', 'Error')
->assign('message', 'Internal server error')
->assign('debug', self::$cfg->debug ? $exception : null)
->render('errors/500')
->withStatus(500);
}
}Input Validation
use Kotchasan\Database;
class ValidationController extends Controller
{
/**
* Validation Rules
*/
protected function getValidationRules()
{
return [
'user_create' => [
'name' => 'required|string|min:2|max:100',
'email' => 'required|email|unique:users,email',
'password' => 'required|string|min:6|confirmed',
'phone' => 'nullable|string|regex:/^[0-9\-\+\s\(\)]+$/',
'age' => 'nullable|integer|min:18|max:100'
],
'user_update' => [
'name' => 'string|min:2|max:100',
'email' => 'email|unique:users,email,{id}',
'password' => 'string|min:6|confirmed',
'phone' => 'nullable|string|regex:/^[0-9\-\+\s\(\)]+$/',
'age' => 'nullable|integer|min:18|max:100'
],
'product_create' => [
'name' => 'required|string|min:2|max:200',
'price' => 'required|numeric|min:0',
'category_id' => 'required|integer|exists:categories,id',
'description' => 'nullable|string|max:1000',
'image' => 'nullable|file|mimes:jpg,jpeg,png|max:2048'
]
];
}
/**
* Validate Input against Rules
*/
protected function validateInput(string $ruleSet, array $input, array $context = []): array
{
$rules = $this->getValidationRules()[$ruleSet] ?? [];
$errors = [];
foreach ($rules as $field => $rule) {
$ruleList = explode('|', $rule);
$value = $input[$field] ?? null;
foreach ($ruleList as $singleRule) {
$error = $this->validateField($field, $value, $singleRule, $input, $context);
if ($error) {
$errors[$field] = $error;
break; // Stop checking other rules for this field
}
}
}
return $errors;
}
/**
* Validate single Field
*/
private function validateField(string $field, $value, string $rule, array $input, array $context): ?string
{
$ruleParts = explode(':', $rule, 2);
$ruleName = $ruleParts[0];
$ruleParam = $ruleParts[1] ?? null;
switch ($ruleName) {
case 'required':
if (empty($value)) {
return "Field {$field} is required";
}
break;
case 'string':
if (!is_string($value)) {
return "Field {$field} must be a string";
}
break;
case 'integer':
if (!is_numeric($value) || !is_int((int)$value)) {
return "Field {$field} must be an integer";
}
break;
case 'numeric':
if (!is_numeric($value)) {
return "Field {$field} must be a number";
}
break;
case 'email':
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
return "Field {$field} must be a valid email";
}
break;
case 'min':
$min = (int)$ruleParam;
if (strlen($value) < $min) {
return "Field {$field} must have at least {$min} characters";
}
break;
case 'max':
$max = (int)$ruleParam;
if (strlen($value) > $max) {
return "Field {$field} must not exceed {$max} characters";
}
break;
case 'unique':
list($table, $column, $except) = explode(',', $ruleParam . ',,');
$except = str_replace('{id}', $context['id'] ?? '', $except);
$query = Kotchasan\Database::create()
->select(['id'])
->from($table)
->where([$column, $value]);
if (!empty($except)) {
$query->where(['id', '!=', $except]);
}
if ($query->first()) {
return "Field {$field} is already taken";
}
break;
case 'exists':
list($table, $column) = explode(',', $ruleParam);
$exists = Kotchasan\Database::create()
->select(['id'])
->from($table)
->where([$column, $value])
->first();
if (!$exists) {
return "Field {$field} not found";
}
break;
case 'confirmed':
$confirmField = $field . '_confirmation';
if ($value !== ($input[$confirmField] ?? null)) {
return "Field {$field} confirmation does not match";
}
break;
}
return null;
}
}Best Practices
1. Controller Organization
// Organize Controllers by feature
// app/Controllers/User/UserController.php
// app/Controllers/Product/ProductController.php
// app/Controllers/Order/OrderController.php
namespace App\Controllers\User;
class UserController extends Controller
{
// Keep logic related to User only
public function index() { / ... / }
public function show() { / ... / }
public function create() { / ... / }
public function update() { / ... / }
public function delete() { / ... / }
}2. Dependency Injection
class UserController extends Controller
{
private $userService;
private $notificationService;
public function __construct(
UserService $userService,
NotificationService $notificationService
) {
$this->userService = $userService;
$this->notificationService = $notificationService;
}
public function create(Request $request): Response
{
$input = $request->getParsedBody();
try {
$user = $this->userService->createUser($input);
$this->notificationService->sendWelcomeEmail($user);
return $this->successResponse($user, 'User created successfully');
} catch (ValidationException $e) {
return $this->errorResponse('Invalid data', 422, $e->getErrors());
}
}
}3. Response Consistency
trait ApiResponseTrait
{
$code = 20; // 0): Response
{
return Response::create()
->withHeader('Content-Type', 'application/json')
->withStatus($code)
->getBody()->write(json_encode([
'success' => true,
'message' => $message,
'data' => $data,
'timestamp' => time()
]));
}
$code = 40; // 0, $errors = null): Response
{
$response = [
'success' => false,
'message' => $message,
'timestamp' => time()
];
if ($errors !== null) {
$response['errors'] = $errors;
}
return Response::create()
->withHeader('Content-Type', 'application/json')
->withStatus($code)
->getBody()->write(json_encode($response));
}
protected function paginatedResponse(array $data, int $total, int $page, int $limit): Response
{
return $this->successResponse([
'items' => $data,
'pagination' => [
'current_page' => $page,
'per_page' => $limit,
'total' => $total,
'total_pages' => ceil($total / $limit),
'has_next' => $page < ceil($total / $limit),
'has_prev' => $page > 1
]
]);
}
}4. Error Handling
class BaseController extends Controller
{
protected function handleException(Exception $e, Request $request): Response
{
// Log error
error_log("Controller Error: " . $e->getMessage());
// Check exception type
if ($e instanceof ValidationException) {
return $this->errorResponse('Invalid data', 422, $e->getErrors());
}
if ($e instanceof AuthenticationException) {
return $this->errorResponse('Unauthorized', 401);
}
if ($e instanceof NotFoundException) {
return $this->errorResponse('Not found', 404);
}
// General Error
$message = self::$cfg->debug ? $e->getMessage() : 'Internal server error';
return $this->errorResponse($message, 500);
}
}5. Performance Optimization
use Kotchasan\Database;
class OptimizedController extends Controller
{
/**
* Use Cache for infrequently changing data
*/
public function getCategories(Request $request): Response
{
$cache = CacheFactory::create('file');
$cacheKey = 'categories_list';
$categories = $cache->get($cacheKey);
if ($categories === null) {
$categories = Kotchasan\Database::create()
->select(['id', 'name', 'slug'])
->from('categories')
->where(['status', 'active'])
->orderBy('name')
->fetchAll();
$cache->set($cacheKey, $categories, 3600); // Cache for 1 hour
}
return $this->successResponse($categories);
}
/**
* Use Eager Loading to reduce N+1 queries
*/
public function getUsersWithProfiles(Request $request): Response
{
// Instead of separate queries for each user
$users = Kotchasan\Database::create()
->select([
'U.id', 'U.name', 'U.email',
'P.phone', 'P.address', 'P.avatar'
])
->from('users U')
->leftJoin('user_profiles P', 'U.id = P.user_id')
->where(['U.status', 'active'])
->fetchAll();
return $this->successResponse($users);
}
}Controller Classes in Kotchasan Framework provide flexibility in handling HTTP requests and creating efficient API endpoints. Proper usage will maintain organized and maintainable code.