# CLQMS Code Style & Conventions ## File Organization ### Directory Structure ``` app/ ├── Controllers/ # API endpoint handlers │ ├── Patient/ # Patient-related controllers │ ├── Organization/ # Organization-related controllers │ ├── Test/ # Test-related controllers │ └── ... ├── Models/ # Data access layer │ ├── BaseModel.php # Base model with UTC normalization │ └── ... ├── Libraries/ # Reusable libraries │ ├── ValueSet.php # JSON-based lookup system │ └── Data/ # ValueSet JSON files ├── Database/ │ └── Migrations/ # Database schema migrations └── Config/ # Configuration files tests/ ├── feature/ # Integration/API tests ├── unit/ # Unit tests └── _support/ # Test utilities public/ ├── api-docs.yaml # OpenAPI/Swagger documentation (CRITICAL to update!) └── index.php # Front controller ``` ## Naming Conventions | Type | Convention | Examples | |------|------------|----------| | **Classes** | PascalCase | `PatientController`, `PatientModel`, `ValueSet` | | **Methods** | camelCase | `getPatient`, `createPatient`, `updatePatient` | | **Variables** | camelCase | `$patientId`, `$rows`, `$input` | | **Database Fields** | PascalCase with underscores | `PatientID`, `NameFirst`, `Street_1`, `InternalPID` | | **Constants** | UPPER_SNAKE_CASE | `MAX_ATTEMPTS`, `DEFAULT_PRIORITY` | | **Private Methods** | Prefix with underscore if needed | `_validatePatient` | | **Files** | PascalCase | `PatientController.php`, `PatientModel.php` | ## Formatting & Style ### Indentation & Braces - **2-space indentation** for all code - **Same-line opening braces**: `public function index() {` - No trailing whitespace - Closing braces on new line ### Imports & Namespaces ```php ` for complex arrays when clear ## Controllers Pattern ### Standard Controller Structure ```php db = \Config\Database::connect(); $this->model = new PatientModel(); $this->rules = [ 'NameFirst' => 'required|regex_match[/^[A-Za-z\'\. ]+$/]|min_length[1]|max_length[60]', // More validation rules... ]; } public function index() { $filters = [ 'InternalPID' => $this->request->getVar('InternalPID'), 'PatientID' => $this->request->getVar('PatientID'), ]; try { $rows = $this->model->getPatients($filters); $rows = ValueSet::transformLabels($rows, [ 'Sex' => 'sex', ]); return $this->respond([ 'status' => 'success', 'message' => 'data fetched successfully', 'data' => $rows ], 200); } catch (\Exception $e) { return $this->failServerError('Exception : ' . $e->getMessage()); } } public function create() { $input = $this->request->getJSON(true); if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); } try { $InternalPID = $this->model->createPatient($input); return $this->respondCreated([ 'status' => 'success', 'message' => "data $InternalPID created successfully" ]); } catch (\Exception $e) { return $this->failServerError('Something went wrong: ' . $e->getMessage()); } } } ``` ### Controller Rules - Extend `BaseController` - Use `ResponseTrait` - Define `$this->rules` array for validation - Return JSON: `['status' => 'success', 'message' => '...', 'data' => ...]` - Use try-catch with `$this->failServerError()` - Input: `$this->request->getJSON(true)` or `$this->request->getVar()` ### Response Formats ```php // Success return $this->respond([ 'status' => 'success', 'message' => 'Operation completed', 'data' => $data ], 200); // Created return $this->respondCreated([ 'status' => 'success', 'message' => 'Resource created' ]); // Validation Error return $this->failValidationErrors($errors); // Not Found return $this->failNotFound('Resource not found'); // Server Error return $this->failServerError('Error message'); ``` ## Models Pattern ### Standard Model Structure ```php select('InternalPID, PatientID, NameFirst, NameLast, Sex'); if (!empty($filters['PatientID'])) { $this->like('PatientID', $filters['PatientID'], 'both'); } $rows = $this->findAll(); $rows = ValueSet::transformLabels($rows, [ 'Sex' => 'sex', ]); return $rows; } public function createPatient(array $input) { $db = \Config\Database::connect(); $db->transBegin(); try { $this->insert($input); $newInternalPID = $this->getInsertID(); $this->checkDbError($db, 'Insert patient'); // Additional operations... $db->transCommit(); return $newInternalPID; } catch (\Exception $e) { $db->transRollback(); throw $e; } } private function checkDbError($db, string $context) { $error = $db->error(); if (!empty($error['code'])) { throw new \Exception( "{$context} failed: {$error['code']} - {$error['message']}" ); } } } ``` ### Model Rules - Extend `BaseModel` (auto UTC date normalization) - Define `$table`, `$primaryKey`, `$allowedFields` - Use soft deletes: `$useSoftDeletes = true`, `$deletedField = 'DelDate'` - Wrap multi-table operations in transactions - Use `$this->checkDbError($db, 'context')` after DB operations ## Database Operations ### Query Builder (Preferred) ```php // Select with joins $this->select('patient.*, patcom.Comment') ->join('patcom', 'patcom.InternalPID = patient.InternalPID', 'left') ->where('patient.InternalPID', (int) $InternalPID) ->findAll(); // Insert $this->insert($data); // Update $this->where('InternalPID', $id)->set($data)->update(); // Delete (soft) $this->where('InternalPID', $id)->delete(); // Sets DelDate ``` ### Escape Inputs ```php // Escape for raw queries (rarely used) $this->db->escape($value) // Better: Use parameter binding $this->where('PatientID', $patientId)->get(); ``` ## ValueSet Usage ### For Static Lookups ```php use App\Libraries\ValueSet; // Get all values for a lookup (formatted for dropdowns) $gender = ValueSet::get('sex'); // Returns: [{"value"=>"1","label":"Female"},{"value"=>"2","label":"Male"},...] // Get single label by key $label = ValueSet::getLabel('sex', '1'); // Returns 'Female' // Transform database results to add text labels $patients = [ ['ID' => 1, 'Sex' => '1'], ['ID' => 2, 'Sex' => '2'], ]; $labeled = ValueSet::transformLabels($patients, [ 'Sex' => 'sex' ]); // Result: [['ID'=>1, 'Sex'=>'1', 'SexLabel'=>'Female'], ...] // Clear cache after modifying valueset JSON files ValueSet::clearCache(); ``` ### ValueSet JSON File Format ```json { "name": "sex", "description": "Patient gender", "values": [ {"key": "1", "value": "Female"}, {"key": "2", "value": "Male"}, {"key": "3", "value": "Unknown"} ] } ``` ## Error Handling ### Validation ```php if (!$this->validateData($input, $rules)) { return $this->failValidationErrors($this->validator->getErrors()); } ``` ### Try-Catch Pattern ```php try { $result = $this->model->createPatient($input); return $this->respondCreated([ 'status' => 'success', 'message' => 'Created successfully' ]); } catch (\Exception $e) { return $this->failServerError('Something went wrong: ' . $e->getMessage()); } ``` ### HTTP Status Codes - `200` - Success - `201` - Created - `400` - Bad Request (validation errors) - `401` - Unauthorized - `404` - Not Found - `500` - Internal Server Error ## Testing Pattern ### Feature Test ```php 'localhost', 'aud' => 'localhost', 'iat' => time(), 'exp' => time() + 3600, 'uid' => 1, 'email' => 'admin@admin.com' ]; $this->token = \Firebase\JWT\JWT::encode($payload, $key, 'HS256'); } public function testCanCreatePatient() { $this->withHeaders(['Cookie' => 'token=' . $this->token]) ->post('api/patient', [ 'PatientID' => 'PT001', 'NameFirst' => 'John', 'NameLast' => 'Doe', 'Sex' => '2', 'Birthdate' => '1990-05-15' ]) ->assertStatus(200) ->assertJSONFragment(['status' => 'success']); } } ``` ### Test Commands ```bash # Run all tests vendor/bin/phpunit # Run specific test file vendor/bin/phpunit tests/feature/Patient/PatientCreateTest.php # Run with coverage vendor/bin/phpunit --coverage-text=tests/coverage.txt --coverage-html=tests/coverage/ -d memory_limit=1024m ``` ## Special Considerations ### UTC Date Handling - BaseModel automatically normalizes dates to UTC - All dates in database are in UTC format - API responses return dates in ISO 8601 format: `Y-m-d\TH:i:s\Z` - Date conversions handled automatically via BaseModel hooks ### Soft Deletes - All transactional tables use soft deletes via `DelDate` field - Soft delete sets `DelDate` to current timestamp - Query Builder automatically filters out deleted records ### Input Validation - Use CodeIgniter's validation rules in controllers - Custom regex patterns for specific formats (e.g., KTP, Passport) - Validate before processing in controllers ### API Documentation (CRITICAL) - After modifying ANY controller, you MUST update `public/api-docs.yaml` - Update OpenAPI schema definitions for new/changed endpoints - Update field names, types, and response formats - Ensure schemas match actual controller responses ### Security - Never log or commit secrets (JWT_SECRET, passwords) - Escape user inputs before DB operations - Use JWT authentication for API endpoints - Validate all inputs before processing ## Common Patterns ### Nested Data Handling ```php // Extract nested data before filtering $patIdt = $input['PatIdt'] ?? null; $patCom = $input['PatCom'] ?? null; // Remove nested arrays that don't belong to parent table unset($input['PatIdt'], $input['PatCom']); // Process nested data separately if (!empty($patIdt)) { $modelPatIdt->createPatIdt($patIdt, $newInternalPID); } ``` ### Foreign Key Handling ```php // Handle array-based foreign keys if (!empty($input['LinkTo']) && is_array($input['LinkTo'])) { $internalPids = array_column($input['LinkTo'], 'InternalPID'); $input['LinkTo'] = implode(',', $internalPids); } $input['LinkTo'] = empty($input['LinkTo']) ? null : $input['LinkTo']; ``` ### Dynamic Validation Rules ```php // Override validation rules based on input type $type = $input['PatIdt']['IdentifierType'] ?? null; $identifierRulesMap = [ 'KTP' => 'required|regex_match[/^[0-9]{16}$/]', 'PASS' => 'required|regex_match[/^[A-Za-z0-9]{1,9}$/]', ]; if ($type) { $this->rules['PatIdt.Identifier'] = $identifierRulesMap[$type] ?? 'permit_empty|max_length[255]'; } ```