Kotchasan Framework Documentation
Input and Request Handling
Input and Request Handling
Kotchasan Framework provides a comprehensive and modern Input and Request handling system that supports PSR-7 HTTP Message Interface with additional features for developing secure and efficient web applications.
Table of Contents
- Request Class - HTTP Request Management
- Input Class - Input Data Processing
- InputItem Class - Individual Data Handling
- Response Class - HTTP Response Creation
- Parameter Management and Validation
- File Upload Handling
- Response Formatting
- Real-world Usage Examples
Request Class - HTTP Request Management
Creating Request Objects
use Kotchasan\Http\Request;
// Create Request from server variables
$request = new Request();
// Create Request without superglobals data
$request = new Request(false);
// Use Request in PSR-7 style
$newRequest = $request->withQueryParams(['page' => 2]);
$anotherRequest = $request->withParsedBody(['name' => 'John']);Retrieving HTTP Method and Basic Information
// Check HTTP Method
$method = $request->getMethod(); // GET, POST, PUT, DELETE, etc.
// Method Override Support (for forms that don't support PUT/DELETE)
// 1. Via X-HTTP-Method-Override header
// 2. Via _method parameter in POST
$isSecure = $request->isSecure(); // true if HTTPS
$protocol = $request->getProtocolVersion(); // 1.0, 1.1, 2.0
$uri = $request->getUri(); // URI object
// Check request types
$isAjax = $request->isAjax();
$isJson = $request->isJson();
$isGet = $request->isMethod('GET');
$isPost = $request->isMethod('POST');Retrieving Parameters
// Retrieve GET parameters
$id = $request->get('id'); // Returns InputItem
$page = $request->get('page', 1); // Default value 1
// Retrieve POST parameters
$name = $request->post('name');
$email = $request->post('email', '');
// Retrieve combined data (POST -> GET -> Cookie)
$value = $request->request('key', 'default_value');
// Retrieve raw data (without wrapper)
$rawValue = $request->postRaw('data');
$rawGet = $request->getRaw('param');
// Retrieve Server parameters
$userAgent = $request->server('HTTP_USER_AGENT');
$contentType = $request->server('CONTENT_TYPE');
$remoteAddr = $request->server('REMOTE_ADDR');Header Management
// Retrieve Headers
$allHeaders = $request->getHeaders();
$contentType = $request->getHeaderLine('Content-Type');
$authorization = $request->getHeaderLine('Authorization');
// Check header existence
if ($request->hasHeader('X-Requested-With')) {
echo "This is an AJAX request";
}
// Add/modify Headers (PSR-7 immutable)
$newRequest = $request->withHeader('Accept', 'application/json');
$requestWithHeaders = $request->withHeaders([
'Accept' => 'application/json',
'X-Custom-Header' => 'custom-value'
]);Body and JSON Handling
// Retrieve Request Body
$body = $request->getBody();
$bodyContent = $body->getContents();
// Retrieve JSON data
if ($request->isJson()) {
$jsonData = $request->getJson();
$specificValue = $request->getJsonData('user.name', 'Unknown');
}
// Retrieve Parsed Body
$parsedBody = $request->getParsedBody(); // array or object
// Check content size
$contentLength = $request->getContentLength();
if ($contentLength > 1024 * 1024) { // 1MB
throw new Exception('Request too large');
}Input Class - Input Data Processing
Using Input Class
use Kotchasan\Input;
// Create Input instance
$input = new Input($request);
// Or use static method
$input = Input::create($request);
// Immutable usage (PSR-7)
$newInput = $input->withRequest($newRequest);
$inputWithQuery = $input->withQueryParams(['sort' => 'name']);
$inputWithBody = $input->withParsedBody(['action' => 'save']);Retrieving Data from Different Sources
// Retrieve data from specific sources
$postData = $input->get('username', '', 'post');
$getData = $input->get('page', 1, 'get');
$cookieData = $input->get('session_id', '', 'cookie');
$serverData = $input->get('HTTP_HOST', '', 'server');
$jsonData = $input->get('data', [], 'json');
// Retrieve data from all sources (order: POST -> GET -> JSON)
$value = $input->get('key', 'default'); // for 'all' source
// Retrieve all data from specific source
$allPost = $input->all('post');
$allGet = $input->all('get');
$allCookies = $input->all('cookie');
$allServer = $input->all('server');
$allJson = $input->all('json');
$allData = $input->all(); // combined all;Type-safe Data Usage
// Retrieve data with type conversion
$userId = $input->getInt('user_id', 0);
$price = $input->getFloat('price', 0.0);
$isActive = $input->getBool('active', false);
$tags = $input->getArray('tags', []);
$description = $input->getString('description', '');
// Retrieve special type data
$email = $input->getEmail('email');
$url = $input->getUrl('website');
$phone = $input->getPhone('phone');
$date = $input->getDate('birth_date');
$time = $input->getTime('appointment_time');
// Retrieve nested data from JSON
$cityName = $input->getNestedValue('address.city', 'Unknown');
$coordinates = $input->getNestedArray('location.coordinates', []);Security Validation
// Data sanitization
$cleanInput = $input->sanitize($userInput);
$cleanArray = $input->sanitizeArray($userArray);
// CSRF Token validation
$token = $input->getString('csrf_token');
if (!$input->validateCsrfToken($token)) {
throw new SecurityException('Invalid CSRF token');
}
// Generate CSRF Token
$csrfToken = $input->generateCsrfToken();
// Rate limiting validation
if (!$input->checkRateLimit('login', 5, 300)) { // 5 attempts in 5 minutes
throw new Exception('Too many attempts');
}InputItem Class - Individual Data Handling
Using InputItem
// InputItem is automatically created when using get() methods
$username = $request->get('username'); // Returns InputItem
// Type conversion
$asString = $username->toString();
$asInt = $username->toInt();
$asFloat = $username->toFloat();
$asBool = $username->toBool();
$asArray = $username->toArray();
// Validation and sanitization
$filtered = $username->filter(); // Filter dangerous content
$alphanumeric = $username->alphanumeric(); // Only letters and numbers
$oneLine = $username->oneLine(); // Remove newlines and tabs
$truncated = $username->cut(50); // Truncate to 50 characters
// Format validation
$cleanEmail = $username->email(); // Validate email format
$cleanUrl = $username->url(); // Validate URL format
$cleanPhone = $username->phone(); // Validate phone format
$cleanDate = $username->date(); // Validate date format
// Text processing
$topic = $username->topic(); // For topics
$detail = $username->detail(); // For details
$description = $username->description(); // For descriptions
$keywords = $username->keywords(); // For keywords
$textarea = $username->textarea(); // For textarea
// Existence checking
$exists = $username->exists(); // Check if value exists
$isEmpty = $username->isEmpty(); // Check if empty;Fluent Interface Usage
// Chain methods for data processing
$processedValue = $request->get('user_input')
->filter() // Filter dangerous content
->oneLine() // Remove newlines
->cut(100) // Truncate to 100 characters
->toString(); // Convert to string
$cleanNumber = $request->get('amount')
->number() // Validate as number
->toFloat(); // Convert to float
$validatedData = $request->get('data')
->json() // Validate JSON format
->toArray(); // Convert to array;Response Class - HTTP Response Creation
Basic Response Creation
use Kotchasan\Http\Response;
// Create new Response
$response = new Response();
// Set Status Code
$response = $response->withStatus(200); // OK
$response = $response->withStatus(404, 'Not Found');
$response = $response->withStatus(500, 'Internal Server Error');
// Set Headers
$response = $response->withHeader('Content-Type', 'application/json');
$response = $response->withHeaders([
'Cache-Control' => 'no-cache',
'X-Frame-Options' => 'DENY'
]);
// Set Content
$response = $response->withContent('Hello World');
$content = $response->getContent();Creating JSON Responses
// Basic JSON Response
$data = ['message' => 'Success', 'data' => $userData];
$jsonResponse = $response->json($data, 200);
// Static methods for JSON
$successResponse = Response::makeJson(['success' => true], 200);
$errorResponse = Response::makeJson(['error' => 'Not found'], 404);
// Predefined JSON responses
$okResponse = Response::makeOk(['data' => $data]);
$createdResponse = Response::makeCreated(['id' => $newId]);
$badRequestResponse = Response::makeBadRequest(['error' => 'Invalid input']);
$unauthorizedResponse = Response::makeUnauthorized(['error' => 'Login required']);
$forbiddenResponse = Response::makeForbidden(['error' => 'Access denied']);
$notFoundResponse = Response::makeNotFound(['error' => 'Resource not found']);
$serverErrorResponse = Response::makeServerError(['error' => 'Internal error']);Creating HTML Responses
// HTML Response
$htmlContent = '<html><body><h1>Welcome</h1></body></html>';
$htmlResponse = $response->html($htmlContent, 200);
// HTML Response with template
$templateData = ['title' => 'Home', 'content' => 'Welcome'];
$renderedHtml = $view->render('home', $templateData);
$htmlResponse = $response->html($renderedHtml);Creating Redirect Responses
// Redirect responses
$redirectResponse = $response->redirect('/new-location'); // 302 Found
$permanentRedirect = $response->redirect('/new-location', 301); // 301 Moved Permanently
// Redirect with flash message
$redirectWithMessage = $response->redirect('/dashboard')
->withHeader('X-Flash-Message', 'Login successful');Setting Security Headers
// Set basic security headers
$secureResponse = $response->setSecurityHeaders();
// Or set manually
$secureResponse = $response->withHeaders([
'X-Content-Type-Options' => 'nosniff',
'X-Frame-Options' => 'DENY',
'X-XSS-Protection' => '1; mode=block',
'Content-Security-Policy' => "default-src 'self'",
'Strict-Transport-Security' => 'max-age=31536000; includeSubDomains'
]);
// Set CORS headers
$corsResponse = $response->setCorsHeaders();
// Or set CORS manually
$corsResponse = $response->withHeaders([
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers' => 'Content-Type, Authorization'
]);
// Set no-cache headers
$noCacheResponse = $response->setNoCacheHeaders();Creating File Download Responses
// File download
$fileContent = file_get_contents('/path/to/file.pdf');
$downloadResponse = $response->download($fileContent, 'document.pdf', 'application/pdf');
// File stream (for large files)
$filePath = '/path/to/large-file.zip';
$fileResponse = $response->file($filePath, 'archive.zip');
// Image response
$imageContent = file_get_contents('/path/to/image.jpg');
$imageResponse = $response->withContent($imageContent)
->withHeader('Content-Type', 'image/jpeg')
->withHeader('Content-Disposition', 'inline; filename="image.jpg"');Parameter Management and Validation
Parameter Validation and Checking
class UserController
{
public function updateProfile(Request $request): Response
{
$input = new Input($request);
// Check required parameters
$requiredParams = ['name', 'email'];
foreach ($requiredParams as $param) {
if (!$input->get($param)->exists()) {
return Response::makeBadRequest([
'error' => "Missing required parameter: {$param}"
]);
}
}
// Validate data format
$name = $input->getString('name');
if (strlen($name) < 2) {
return Response::makeBadRequest([
'error' => 'Name must be at least 2 characters'
]);
}
$email = $input->getEmail('email');
if (empty($email)) {
return Response::makeBadRequest([
'error' => 'Invalid email format'
]);
}
// Validate CSRF token
if (!$input->validateCsrfToken($input->getString('csrf_token'))) {
return Response::makeForbidden(['error' => 'Invalid CSRF token']);
}
// Process data
$userData = [
'name' => $name,
'email' => $email,
'phone' => $input->getPhone('phone'),
'birth_date' => $input->getDate('birth_date'),
'is_active' => $input->getBool('is_active', true)
];
// Save data
$this->userService->updateProfile($userData);
return Response::makeOk(['message' => 'Profile updated successfully']);
}
}Pagination Management
class ProductController
{
public function getProducts(Request $request): Response
{
$input = new Input($request);
// Handle pagination parameters
$page = max(1, $input->getInt('page', 1));
$limit = min(100, max(10, $input->getInt('limit', 20))); // Limit 10-100
$sort = $input->getString('sort', 'created_at');
$order = in_array($input->getString('order'), ['asc', 'desc'])
? $input->getString('order')
: 'desc';
// Data filtering
$filters = [];
if ($input->get('category')->exists()) {
$filters['category_id'] = $input->getInt('category');
}
if ($input->get('search')->exists()) {
$filters['search'] = $input->getString('search');
}
if ($input->get('price_min')->exists()) {
$filters['price_min'] = $input->getFloat('price_min');
}
if ($input->get('price_max')->exists()) {
$filters['price_max'] = $input->getFloat('price_max');
}
// Retrieve data
$offset = ($page - 1) $limit;
$products = $this->productService->getProducts($filters, $sort, $order, $limit, $offset);
$total = $this->productService->countProducts($filters);
// Create pagination metadata
$pagination = [
'current_page' => $page,
'per_page' => $limit,
'total' => $total,
'total_pages' => ceil($total / $limit),
'has_more' => ($page $limit) < $total
];
return Response::makeOk([
'products' => $products,
'pagination' => $pagination,
'filters' => $filters
]);
}
}File Upload Handling
Basic File Upload Management
class FileUploadController
{
public function uploadAvatar(Request $request): Response
{
$input = new Input($request);
// Check uploaded files
$uploadedFiles = $request->getUploadedFiles();
if (!isset($uploadedFiles['avatar'])) {
return Response::makeBadRequest(['error' => 'No file uploaded']);
}
$file = $uploadedFiles['avatar'];
// Check for errors
if ($file->getError() !== UPLOAD_ERR_OK) {
return Response::makeBadRequest(['error' => 'File upload failed']);
}
// Check file size
$maxSize = 2; // 1024 1024; // 2MB
if ($file->getSize() > $maxSize) {
return Response::makeBadRequest(['error' => 'File too large']);
}
// Check file type
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
$mimeType = $file->getClientMediaType();
if (!in_array($mimeType, $allowedTypes)) {
return Response::makeBadRequest(['error' => 'Invalid file type']);
}
// Check file security
if (!Validator::isFileSafe($file)) {
return Response::makeBadRequest(['error' => 'File contains malicious content']);
}
// Save file
$filename = uniqid() . '_' . $file->getClientFilename();
$uploadPath = '/uploads/avatars/' . $filename;
$file->moveTo($uploadPath);
// Save to database
$fileInfo = [
'original_name' => $file->getClientFilename(),
'filename' => $filename,
'path' => $uploadPath,
'size' => $file->getSize(),
'mime_type' => $mimeType,
'uploaded_at' => date('Y-m-d H:i:s')
];
$fileId = $this->fileService->saveFileInfo($fileInfo);
return Response::makeCreated([
'file_id' => $fileId,
'url' => '/uploads/avatars/' . $filename,
'message' => 'File uploaded successfully'
]);
}
}Multiple File Upload
class MultiFileUploadController
{
public function uploadDocuments(Request $request): Response
{
$input = new Input($request);
$uploadedFiles = $request->getUploadedFiles();
if (!isset($uploadedFiles['documents']) || !is_array($uploadedFiles['documents'])) {
return Response::makeBadRequest(['error' => 'No files uploaded']);
}
$files = $uploadedFiles['documents'];
$uploadedFileInfo = [];
$errors = [];
foreach ($files as $index => $file) {
try {
// Validate each file
if ($file->getError() !== UPLOAD_ERR_OK) {
$errors[] = "File {$index}: Upload failed";
continue;
}
// Check size
if ($file->getSize() > 10 1024 1024) { // 10MB
$errors[] = "File {$index}: Too large";
continue;
}
// Check type
$allowedTypes = ['application/pdf', 'application/msword', 'text/plain'];
if (!in_array($file->getClientMediaType(), $allowedTypes)) {
$errors[] = "File {$index}: Invalid type";
continue;
}
// Save file
$filename = uniqid() . '_' . $file->getClientFilename();
$uploadPath = '/uploads/documents/' . $filename;
$file->moveTo($uploadPath);
$uploadedFileInfo[] = [
'original_name' => $file->getClientFilename(),
'filename' => $filename,
'path' => $uploadPath,
'size' => $file->getSize(),
'mime_type' => $file->getClientMediaType()
];
} catch (Exception $e) {
$errors[] = "File {$index}: {$e->getMessage()}";
}
}
if (empty($uploadedFileInfo) && !empty($errors)) {
return Response::makeBadRequest([
'error' => 'No files were uploaded successfully',
'details' => $errors
]);
}
return Response::makeOk([
'uploaded_files' => $uploadedFileInfo,
'errors' => $errors,
'message' => count($uploadedFileInfo) . ' files uploaded successfully'
]);
}
}Response Formatting
API Response Format Standards
class ApiResponseFormatter
{
/
Create standard Success Response
/
public static function success($data = null, string $message = 'Success', int $status = 200): Response
{
$response = [
'success' => true,
'message' => $message,
'timestamp' => date('c'),
'request_id' => uniqid()
];
if ($data !== null) {
$response['data'] = $data;
}
return Response::makeJson($response, $status);
}
/
Create standard Error Response
/
public static function error(string $message, int $status = 400, array $errors = [], $code = null): Response
{
$response = [
'success' => false,
'message' => $message,
'timestamp' => date('c'),
'request_id' => uniqid()
];
if (!empty($errors)) {
$response['errors'] = $errors;
}
if ($code !== null) {
$response['error_code'] = $code;
}
return Response::makeJson($response, $status);
}
/
Create Paginated Response
/
public static function paginated(array $items, int $total, int $page, int $limit, string $message = 'Data retrieved'): Response
{
$totalPages = ceil($total / $limit);
$data = [
'items' => $items,
'pagination' => [
'current_page' => $page,
'per_page' => $limit,
'total_items' => $total,
'total_pages' => $totalPages,
'has_next' => $page < $totalPages,
'has_prev' => $page > 1,
'next_page' => $page < $totalPages ? $page + 1 : null,
'prev_page' => $page > 1 ? $page - 1 : null
]
];
return self::success($data, $message);
}
/
Create Validation Error Response
/
public static function validationError(array $errors, string $message = 'Validation failed'): Response
{
return self::error($message, 422, $errors, 'VALIDATION_ERROR');
}
}
// Usage
class UserApiController
{
public function getUsers(Request $request): Response
{
$input = new Input($request);
try {
$page = $input->getInt('page', 1);
$limit = $input->getInt('limit', 20);
$users = $this->userService->getUsers($page, $limit);
$total = $this->userService->getTotalUsers();
return ApiResponseFormatter::paginated($users, $total, $page, $limit);
} catch (Exception $e) {
return ApiResponseFormatter::error('Failed to retrieve users', 500, [], 'SERVER_ERROR');
}
}
public function createUser(Request $request): Response
{
$input = new Input($request);
// Validation
$validation = $this->validateUserInput($input);
if (!empty($validation['errors'])) {
return ApiResponseFormatter::validationError($validation['errors']);
}
try {
$user = $this->userService->createUser($validation['data']);
return ApiResponseFormatter::success($user, 'User created successfully', 201);
} catch (Exception $e) {
return ApiResponseFormatter::error('Failed to create user', 500);
}
}
}Content Negotiation
class ContentNegotiationController
{
public function getResource(Request $request): Response
{
$input = new Input($request);
$acceptHeader = $request->getHeaderLine('Accept');
// Retrieve data
$data = $this->resourceService->getData();
// Decide response format based on Accept header
if (strpos($acceptHeader, 'application/json') !== false) {
return Response::makeJson($data);
}
elseif (strpos($acceptHeader, 'application/xml') !== false) {
$xml = $this->convertToXml($data);
return (new Response())
->withContent($xml)
->withHeader('Content-Type', 'application/xml');
}
elseif (strpos($acceptHeader, 'text/csv') !== false) {
$csv = $this->convertToCsv($data);
return (new Response())
->withContent($csv)
->withHeader('Content-Type', 'text/csv')
->withHeader('Content-Disposition', 'attachment; filename="data.csv"');
}
else {
// Default to HTML
$html = $this->renderHtml($data);
return (new Response())->html($html);
}
}
private function convertToXml(array $data): string
{
$xml = new SimpleXMLElement('<root/>');
array_walk_recursive($data, function($value, $key) use ($xml) {
$xml->addChild($key, htmlspecialchars($value));
});
return $xml->asXML();
}
private function convertToCsv(array $data): string
{
$output = fopen('php://temp', 'w');
if (!empty($data)) {
// Write header
fputcsv($output, array_keys($data[0]));
// Write data
foreach ($data as $row) {
fputcsv($output, $row);
}
}
rewind($output);
$csv = stream_get_contents($output);
fclose($output);
return $csv;
}
}Real-world Usage Examples
REST API System for User Management
class UserRestController
{
private $userService;
public function __construct(UserService $userService)
{
$this->userService = $userService;
}
/
GET /api/users - Get user list
/
public function index(Request $request): Response
{
$input = new Input($request);
// Handle query parameters
$filters = [
'search' => $input->getString('search', ''),
'role' => $input->getString('role', ''),
'status' => $input->getString('status', 'active'),
'created_from' => $input->getDate('created_from'),
'created_to' => $input->getDate('created_to')
];
$page = max(1, $input->getInt('page', 1));
$limit = min(100, max(10, $input->getInt('limit', 20)));
$sort = $input->getString('sort', 'created_at');
$order = in_array($input->getString('order'), ['asc', 'desc']) ? $input->getString('order') : 'desc';
try {
$users = $this->userService->getUsers($filters, $sort, $order, $limit, $page);
$total = $this->userService->countUsers($filters);
return ApiResponseFormatter::paginated($users, $total, $page, $limit);
} catch (Exception $e) {
return ApiResponseFormatter::error('Failed to retrieve users', 500);
}
}
/
GET /api/users/{id} - Get single user
/
public function show(Request $request, int $id): Response
{
try {
$user = $this->userService->getUserById($id);
if (!$user) {
return ApiResponseFormatter::error('User not found', 404, [], 'USER_NOT_FOUND');
}
return ApiResponseFormatter::success($user, 'User retrieved successfully');
} catch (Exception $e) {
return ApiResponseFormatter::error('Failed to retrieve user', 500);
}
}
/
POST /api/users - Create new user
/
public function store(Request $request): Response
{
$input = new Input($request);
// Validate CSRF token
if (!$input->validateCsrfToken($input->getString('csrf_token'))) {
return ApiResponseFormatter::error('Invalid CSRF token', 403, [], 'CSRF_ERROR');
}
// Validation rules
$rules = [
'username' => 'required|min:3|max:20|alphanumeric',
'email' => 'required|email|unique:users',
'password' => 'required|min:8|password_strength',
'first_name' => 'required|min:2|max:50',
'last_name' => 'required|min:2|max:50',
'role' => 'required|in:admin,user,moderator'
];
$validation = $this->validateInput($input, $rules);
if (!$validation['valid']) {
return ApiResponseFormatter::validationError($validation['errors']);
}
try {
$userData = [
'username' => $input->getString('username'),
'email' => $input->getEmail('email'),
'password' => password_hash($input->getString('password'), PASSWORD_DEFAULT),
'first_name' => $input->getString('first_name'),
'last_name' => $input->getString('last_name'),
'role' => $input->getString('role'),
'created_at' => date('Y-m-d H:i:s')
];
$user = $this->userService->createUser($userData);
return ApiResponseFormatter::success($user, 'User created successfully', 201);
} catch (Exception $e) {
return ApiResponseFormatter::error('Failed to create user', 500);
}
}
/
PUT /api/users/{id} - Update user
/
public function update(Request $request, int $id): Response
{
$input = new Input($request);
// Check if user exists
$existingUser = $this->userService->getUserById($id);
if (!$existingUser) {
return ApiResponseFormatter::error('User not found', 404, [], 'USER_NOT_FOUND');
}
// Check edit permission
if (!$this->canEditUser($request, $id)) {
return ApiResponseFormatter::error('Access denied', 403, [], 'ACCESS_DENIED');
}
// Validation (some fields optional for update)
$rules = [
'email' => 'email|unique:users,email,' . $id,
'first_name' => 'min:2|max:50',
'last_name' => 'min:2|max:50',
'role' => 'in:admin,user,moderator'
];
$validation = $this->validateInput($input, $rules);
if (!$validation['valid']) {
return ApiResponseFormatter::validationError($validation['errors']);
}
try {
$updateData = [];
// Update only sent fields
if ($input->get('email')->exists()) {
$updateData['email'] = $input->getEmail('email');
}
if ($input->get('first_name')->exists()) {
$updateData['first_name'] = $input->getString('first_name');
}
if ($input->get('last_name')->exists()) {
$updateData['last_name'] = $input->getString('last_name');
}
if ($input->get('role')->exists()) {
$updateData['role'] = $input->getString('role');
}
// Update password if provided
if ($input->get('password')->exists()) {
$password = $input->getString('password');
if (strlen($password) >= 8) {
$updateData['password'] = password_hash($password, PASSWORD_DEFAULT);
}
}
$updateData['updated_at'] = date('Y-m-d H:i:s');
$updatedUser = $this->userService->updateUser($id, $updateData);
return ApiResponseFormatter::success($updatedUser, 'User updated successfully');
} catch (Exception $e) {
return ApiResponseFormatter::error('Failed to update user', 500);
}
}
/
DELETE /api/users/{id} - Delete user
/
public function destroy(Request $request, int $id): Response
{
// Check if user exists
$user = $this->userService->getUserById($id);
if (!$user) {
return ApiResponseFormatter::error('User not found', 404, [], 'USER_NOT_FOUND');
}
// Check delete permission
if (!$this->canDeleteUser($request, $id)) {
return ApiResponseFormatter::error('Access denied', 403, [], 'ACCESS_DENIED');
}
try {
$this->userService->deleteUser($id);
return ApiResponseFormatter::success(null, 'User deleted successfully');
} catch (Exception $e) {
return ApiResponseFormatter::error('Failed to delete user', 500);
}
}
private function validateInput(Input $input, array $rules): array
{
$input->rules($rules);
$valid = $input->validate();
return [
'valid' => $valid,
'errors' => $valid ? [] : $input->errors(),
'data' => $valid ? $input->validated() : []
];
}
private function canEditUser(Request $request, int $userId): bool
{
// Check permission from JWT token or session
$currentUserId = $this->getCurrentUserId($request);
$currentUserRole = $this->getCurrentUserRole($request);
// Admin can edit anyone, user can edit only themselves
return $currentUserRole === 'admin' || $currentUserId === $userId;
}
private function canDeleteUser(Request $request, int $userId): bool
{
$currentUserRole = $this->getCurrentUserRole($request);
return $currentUserRole === 'admin';
}
}File Management System
class FileManagementController
{
/
POST /api/files/upload - Upload file
/
public function upload(Request $request): Response
{
$input = new Input($request);
// Check upload permission
if (!$this->canUpload($request)) {
return ApiResponseFormatter::error('Upload permission denied', 403);
}
$uploadedFiles = $request->getUploadedFiles();
if (empty($uploadedFiles['file'])) {
return ApiResponseFormatter::error('No file uploaded', 400);
}
$file = $uploadedFiles['file'];
// Validate file
$validation = $this->validateFile($file, $input);
if (!$validation['valid']) {
return ApiResponseFormatter::validationError($validation['errors']);
}
try {
// Create unique filename
$extension = pathinfo($file->getClientFilename(), PATHINFO_EXTENSION);
$filename = uniqid() . '_' . time() . '.' . $extension;
// Determine upload path by file type
$uploadDir = $this->getUploadDirectory($file->getClientMediaType());
$fullPath = $uploadDir . '/' . $filename;
// Move file
$file->moveTo($fullPath);
// Save to database
$fileData = [
'original_name' => $file->getClientFilename(),
'filename' => $filename,
'path' => $fullPath,
'size' => $file->getSize(),
'mime_type' => $file->getClientMediaType(),
'uploaded_by' => $this->getCurrentUserId($request),
'upload_ip' => $request->server('REMOTE_ADDR'),
'created_at' => date('Y-m-d H:i:s')
];
$fileId = $this->fileService->saveFile($fileData);
// Create thumbnail for images
if (strpos($file->getClientMediaType(), 'image/') === 0) {
$this->createThumbnail($fullPath, $filename);
}
return ApiResponseFormatter::success([
'file_id' => $fileId,
'filename' => $filename,
'original_name' => $file->getClientFilename(),
'size' => $file->getSize(),
'url' => $this->getFileUrl($filename),
'thumbnail_url' => $this->getThumbnailUrl($filename)
], 'File uploaded successfully', 201);
} catch (Exception $e) {
return ApiResponseFormatter::error('Upload failed: ' . $e->getMessage(), 500);
}
}
/
GET /api/files/{id}/download - Download file
/
public function download(Request $request, int $fileId): Response
{
try {
$file = $this->fileService->getFileById($fileId);
if (!$file) {
return ApiResponseFormatter::error('File not found', 404);
}
// Check download permission
if (!$this->canDownload($request, $file)) {
return ApiResponseFormatter::error('Download permission denied', 403);
}
// Check if file exists on disk
if (!file_exists($file['path'])) {
return ApiResponseFormatter::error('File not found on disk', 404);
}
// Update download count
$this->fileService->incrementDownloadCount($fileId);
// Send file
$response = new Response();
return $response->file($file['path'], $file['original_name']);
} catch (Exception $e) {
return ApiResponseFormatter::error('Download failed', 500);
}
}
/
GET /api/files - List files
/
public function index(Request $request): Response
{
$input = new Input($request);
$filters = [
'mime_type' => $input->getString('type', ''),
'search' => $input->getString('search', ''),
'uploaded_by' => $input->getInt('user_id', 0),
'date_from' => $input->getDate('date_from'),
'date_to' => $input->getDate('date_to')
];
$page = max(1, $input->getInt('page', 1));
$limit = min(50, max(10, $input->getInt('limit', 20)));
try {
$files = $this->fileService->getFiles($filters, $page, $limit);
$total = $this->fileService->countFiles($filters);
// Add URL data for each file
$filesWithUrls = array_map(function($file) {
$file['url'] = $this->getFileUrl($file['filename']);
$file['thumbnail_url'] = $this->getThumbnailUrl($file['filename']);
return $file;
}, $files);
return ApiResponseFormatter::paginated($filesWithUrls, $total, $page, $limit);
} catch (Exception $e) {
return ApiResponseFormatter::error('Failed to retrieve files', 500);
}
}
private function validateFile($file, Input $input): array
{
$errors = [];
// Check upload error
if ($file->getError() !== UPLOAD_ERR_OK) {
$errors[] = 'File upload failed with error code: ' . $file->getError();
return ['valid' => false, 'errors' => $errors];
}
// Check file size
$maxSize = $this->getMaxFileSize($file->getClientMediaType());
if ($file->getSize() > $maxSize) {
$errors[] = 'File size exceeds limit of ' . ($maxSize / 1024 / 1024) . 'MB';
}
// Check file type
$allowedTypes = $this->getAllowedFileTypes();
if (!in_array($file->getClientMediaType(), $allowedTypes)) {
$errors[] = 'File type not allowed: ' . $file->getClientMediaType();
}
// Check security
if (!Validator::isFileSafe($file)) {
$errors[] = 'File contains potentially malicious content';
}
// Check image dimensions (if image)
if (strpos($file->getClientMediaType(), 'image/') === 0) {
$imageInfo = getimagesize($file->getStream()->getMetadata('uri'));
if ($imageInfo) {
$maxWidth = 4000;
$maxHeight = 4000;
if ($imageInfo[0] > $maxWidth || $imageInfo[1] > $maxHeight) {
$errors[] = "Image dimensions too large. Max: {$maxWidth}x{$maxHeight}";
}
}
}
return [
'valid' => empty($errors),
'errors' => $errors
];
}
}Kotchasan Framework's Input and Request system is comprehensive and modern, supporting the development of secure and efficient web applications with PSR-7 support and additional features necessary for real-world usage.