Kotchasan Framework Documentation

Kotchasan Framework Documentation

Controller Classes

EN 05 Feb 2026 07:02

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

  1. Controller Base Class
  2. ApiController - REST API Controller
  3. HTTP Request and Response Handling
  4. Authentication and Security
  5. Error Handling and Validation
  6. 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.