clqms-be/.serena/memories/code_style_and_conventions.md

6.1 KiB

CLQMS Code Style and Conventions

Naming Conventions

Element Convention Example
Classes PascalCase PatientController, PatientModel
Methods camelCase createPatient(), getPatients()
Properties snake_case (legacy) / camelCase (new) $patient_id / $patientId
Constants UPPER_SNAKE_CASE MAX_RETRY_COUNT
Database Tables snake_case patient, patient_visits, order_tests
Database Columns PascalCase (legacy) PatientID, NameFirst, Birthdate, CreatedAt
JSON Fields PascalCase "PatientID": "123"

File and Directory Structure

Controllers

  • Grouped by domain in subdirectories: app/Controllers/Patient/, app/Controllers/Specimen/
  • Each controller handles CRUD for its entity
  • Use ResponseTrait for standardized responses

Models

  • Grouped by domain: app/Models/Patient/, app/Models/Specimen/
  • Extend BaseModel for automatic UTC date handling
  • Define $table, $primaryKey, $allowedFields
  • Use checkDbError() for database error detection

Code Patterns

Controller Structure

<?php
namespace App\Controllers\Patient;

use App\Traits\ResponseTrait;
use CodeIgniter\Controller;
use App\Models\Patient\PatientModel;

class PatientController extends Controller {
    use ResponseTrait;

    protected $db;
    protected $model;
    protected $rules;

    public function __construct() {
        $this->db = \Config\Database::connect();
        $this->model = new PatientModel();
        $this->rules = [ /* validation rules */ ];
    }

    public function index() { /* ... */ }
    public function create() { /* ... */ }
    public function show($id) { /* ... */ }
    public function update() { /* ... */ }
    public function delete() { /* ... */ }
}

Model Structure

<?php
namespace App\Models\Patient;

use App\Models\BaseModel;
use App\Services\AuditService;

class PatientModel extends BaseModel {
    protected $table = 'patient';
    protected $primaryKey = 'InternalPID';
    protected $allowedFields = ['PatientID', 'NameFirst', ...];
    
    protected $useTimestamps = true;
    protected $createdField = 'CreateDate';
    protected $useSoftDeletes = true;
    protected $deletedField = 'DelDate';

    public function getPatients($filters = []) { /* ... */ }
    public function createPatient($input) { /* ... */ }
    
    private function checkDbError($db, string $context) {
        $error = $db->error();
        if (!empty($error['code'])) {
            throw new \Exception("{$context} failed: {$error['code']} - {$error['message']}");
        }
    }
}

Validation Rules

  • Define in controller constructor as $this->rules
  • Use CodeIgniter validation rules: required, permit_empty, regex_match, max_length, etc.
  • For nested data, override rules dynamically based on input

Response Format

// Success
return $this->respond([
    'status' => 'success',
    'message' => 'Operation completed',
    'data' => $data
], 200);

// Error
return $this->respond([
    'status' => 'failed',
    'message' => 'Error description',
    'data' => []
], 400);

Note: Custom ResponseTrait automatically converts empty strings to null.

Error Handling

  • Use try-catch for JWT and external calls
  • Log errors: log_message('error', $message)
  • Return structured error responses with appropriate HTTP status codes
try {
    $decoded = JWT::decode($token, new Key($key, 'HS256'));
} catch (\Firebase\JWT\ExpiredException $e) {
    return $this->respond(['status' => 'failed', 'message' => 'Token expired'], 401);
} catch (\Exception $e) {
    return $this->respond(['status' => 'failed', 'message' => 'Invalid token'], 401);
}

Database Operations

  • Use CodeIgniter Query Builder or Model methods
  • Use helper('utc') for UTC date conversion
  • Wrap multi-table operations in transactions
$this->db->transStart();
// ... database operations
$this->db->transComplete();

if ($this->db->transStatus() === false) {
    return $this->respond(['status' => 'error', 'message' => 'Transaction failed'], 500);
}

Audit Logging

Use AuditService::logData() for tracking data changes:

AuditService::logData(
    'CREATE|UPDATE|DELETE',
    'table_name',
    (string) $recordId,
    'entity_name',
    null,
    $previousData,
    $newData,
    'Action description',
    ['metadata' => 'value']
);

Route Patterns

$routes->group('api/patient', function ($routes) {
    $routes->get('/', 'Patient\PatientController::index');
    $routes->post('/', 'Patient\PatientController::create');
    $routes->get('(:num)', 'Patient\PatientController::show/$1');
    $routes->patch('/', 'Patient\PatientController::update');
    $routes->delete('/', 'Patient\PatientController::delete');
});

Testing Guidelines

<?php
namespace Tests\Feature\Patients;

use CodeIgniter\Test\FeatureTestTrait;
use CodeIgniter\Test\CIUnitTestCase;
use Faker\Factory;

class PatientCreateTest extends CIUnitTestCase
{
    use FeatureTestTrait;

    protected $endpoint = 'api/patient';

    public function testCreatePatientSuccess()
    {
        $faker = Factory::create('id_ID');
        $payload = [...];
        $result = $this->withBodyFormat('json')->post($this->endpoint, $payload);
        $result->assertStatus(201);
    }
}

Test Naming: test<Action><Scenario><ExpectedResult> (e.g., testCreatePatientValidationFail)

Security Best Practices

  • Use auth filter for protected routes
  • Sanitize user inputs with validation rules
  • Use parameterized queries (CodeIgniter Query Builder handles this)
  • Store secrets in .env, never commit to repository

Legacy Field Naming

Database uses PascalCase columns: PatientID, NameFirst, Birthdate, CreatedAt, UpdatedAt

ValueSet/Lookup Usage

use App\Libraries\Lookups;

// Get all lookups
$allLookups = Lookups::getAll();

// Get single lookup formatted for dropdowns
$gender = Lookups::get('gender');

// Get label for a specific key
$label = Lookups::getLabel('gender', '1'); // Returns 'Female'

// Transform database records with lookup text labels
$labeled = Lookups::transformLabels($data, ['Sex' => 'gender']);