--- description: PHP + Alpine.js SPA-like Application Pattern (CodeIgniter 4 + DaisyUI) --- # PHP + Alpine.js Application Pattern This workflow describes how to build web applications using **PHP (CodeIgniter 4)** for backend with **Alpine.js + DaisyUI + TailwindCSS** for frontend, creating an SPA-like experience with server-rendered views. ## Philosophy **"No-nonsense"** - Keep it simple, avoid over-engineering. This pattern gives you: - Fast development with PHP backend - Reactive UI with Alpine.js (no heavy framework overhead) - Beautiful UI with DaisyUI/TailwindCSS - JWT-based authentication --- ## Technology Stack | Layer | Technology | |-------|------------| | Backend | CodeIgniter 4 (PHP 8.1+) | | Frontend | Alpine.js + DaisyUI 5 + TailwindCSS | | Database | MySQL/MariaDB | | Auth | JWT (stored in HTTP-only cookies) | | Icons | FontAwesome 7 | --- ## Project Structure ``` project/ ├── app/ │ ├── Config/ │ │ └── Routes.php # All routes (pages + API) │ ├── Controllers/ │ │ ├── BaseController.php # Base controller │ │ ├── PagesController.php # Page routes (returns views) │ │ └── [Resource]Controller.php # API controllers │ ├── Models/ │ │ └── [Resource]Model.php # Database models │ ├── Filters/ │ │ └── JwtAuthFilter.php # JWT authentication filter │ └── Views/ │ ├── layout/ │ │ └── main_layout.php # Base layout with sidebar │ ├── [module]/ │ │ ├── [module]_index.php # Main page with x-data │ │ ├── dialog_[name].php # Modal dialogs (included) │ │ └── drawer_[name].php # Drawer components │ └── login.php ├── public/ │ ├── index.php │ └── assets/ │ ├── css/output.css # Compiled TailwindCSS │ └── js/app.js # Alpine.js setup └── .env # Environment config ``` --- ## 1. Backend Patterns ### 1.1 Routes Structure (`app/Config/Routes.php`) Routes are split into: 1. **Public routes** - Login, logout, auth check 2. **Protected page routes** - Views (with `jwt-auth` filter) 3. **API routes** - RESTful JSON endpoints ```php get('/login', 'PagesController::login'); $routes->post('/login', 'AuthController::login'); $routes->get('/logout', 'AuthController::logout'); // Protected page routes (returns views) $routes->group('', ['filter' => 'jwt-auth'], function ($routes) { $routes->get('/', 'PagesController::dashboard'); $routes->get('/patients', 'PagesController::patients'); $routes->get('/requests', 'PagesController::requests'); // Master data pages $routes->get('/master/doctors', 'PagesController::masterDoctors'); }); // API routes (returns JSON) $routes->group('api', function ($routes) { // Resource: patients $routes->get('patients', 'PatientsController::index'); $routes->get('patients/(:num)', 'PatientsController::show/$1'); $routes->post('patients', 'PatientsController::create'); $routes->patch('patients/(:num)', 'PatientsController::update/$1'); // Resource: [resourceName] // Follow same pattern: index, show, create, update }); ``` ### 1.2 Pages Controller (`app/Controllers/PagesController.php`) This controller ONLY returns views. No business logic. ```php 'Dashboard', 'activePage' => 'dashboard' ]); } public function patients() { return view('patients/patients_index', [ 'pageTitle' => 'Patients', 'activePage' => 'patients' ]); } public function requests() { return view('requests/requests_index', [ 'pageTitle' => 'Lab Requests', 'activePage' => 'requests' ]); } } ``` ### 1.3 API Controller Pattern (`app/Controllers/[Resource]Controller.php`) API controllers handle CRUD operations and return JSON. ```php model = new PatientsModel(); $this->rules = [ 'firstName' => 'required|min_length[2]', 'lastName' => 'required|min_length[2]', ]; } /** * GET /api/patients * List all with optional search */ public function index() { $keyword = $this->request->getGet('keyword'); try { $rows = $this->model->search($keyword); return $this->respond([ 'status' => 'success', 'message' => 'fetch success', 'data' => $rows ], 200); } catch (\Exception $e) { return $this->failServerError('Exception: ' . $e->getMessage()); } } /** * GET /api/patients/:id */ public function show($id = null) { try { $row = $this->model->find($id); if (empty($row)) { return $this->respond(['status' => 'success', 'message' => 'not found'], 200); } return $this->respond(['status' => 'success', 'data' => $row], 200); } catch (\Exception $e) { return $this->failServerError('Error: ' . $e->getMessage()); } } /** * POST /api/patients */ public function create() { $input = $this->request->getJSON(true); if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); } $input = camel_to_snake_array($input); // Convert keys to snake_case try { $id = $this->model->insert($input); return $this->respondCreated([ 'status' => 'success', 'message' => 'Created successfully', 'data' => ['id' => $id] ]); } catch (\Exception $e) { return $this->failServerError('Error: ' . $e->getMessage()); } } /** * PATCH /api/patients/:id */ public function update($id = null) { $input = $this->request->getJSON(true); $input = camel_to_snake_array($input); try { $this->model->update($id, $input); return $this->respond(['status' => 'success', 'message' => 'updated']); } catch (\Exception $e) { return $this->failServerError('Error: ' . $e->getMessage()); } } } ``` ### 1.4 Standard API Response Format Always return this structure: ```json { "status": "success|error", "message": "Human readable message", "data": {} // or [] for lists } ``` --- ## 2. Frontend Patterns ### 2.1 Base Layout (`app/Views/layout/main_layout.php`) The layout provides: - Sidebar navigation with Alpine.js state - Top navbar with user info - Content section for page-specific content - Script section for page-specific JavaScript ```php AppName
renderSection('content') ?>
renderSection('script') ?> ``` ### 2.2 Page View Pattern (`app/Views/[module]/[module]_index.php`) Each page extends the layout and defines its Alpine.js component. ```php extend("layout/main_layout"); ?> section("content") ?>

Patients

Manage patient records

include('[module]/dialog_form'); ?>
endSection(); ?> section("script") ?> endSection(); ?> ``` ### 2.3 Dialog Component Pattern (`app/Views/[module]/dialog_form.php`) Dialogs are included in the main page and share x-data context. ```php ``` --- ## 3. Naming Conventions ### 3.1 Files & Directories | Type | Convention | Example | |------|------------|---------| | Views | `snake_case` | `patients_index.php` | | Dialogs | `dialog_[name].php` | `dialog_form.php` | | Drawers | `drawer_[name].php` | `drawer_filter.php` | | Controllers | `PascalCase` | `PatientsController.php` | | Models | `PascalCase` | `PatientsModel.php` | ### 3.2 Variables & Keys | Context | Convention | Example | |---------|------------|---------| | PHP/Database | `snake_case` | `pat_id`, `first_name` | | JavaScript | `camelCase` | `patId`, `firstName` | | Alpine.js x-data | `camelCase` | `showModal`, `fetchList` | ### 3.3 Primary Keys Use format: `{table_singular}_id` | Table | Primary Key | |-------|-------------| | `patients` | `pat_id` | | `requests` | `request_id` | | `master_tests` | `test_id` | --- ## 4. Database Conventions ### 4.1 Standard Columns Every table should have: ```sql `created_at` DATETIME, `updated_at` DATETIME, `deleted_at` DATETIME -- Soft deletes ``` ### 4.2 Status Codes (Single Character) | Code | Meaning | |------|---------| | `P` | Pending | | `I` | In Progress | | `C` | Completed | | `V` | Validated | | `X` | Cancelled | --- ## 5. UI/UX Guidelines ### 5.1 Color Palette | Purpose | Color | TailwindCSS | |---------|-------|-------------| | Primary Action | Emerald | `bg-emerald-600` | | Secondary | Slate | `bg-slate-800` | | Danger | Red | `bg-red-500` | | Info | Blue | `bg-blue-500` | | Warning | Amber | `bg-amber-500` | ### 5.2 Component Patterns - **Cards**: `bg-white rounded-xl border border-slate-100 shadow-sm` - **Buttons**: Use DaisyUI `btn` with custom colors - **Inputs**: Use DaisyUI `input input-bordered` - **Modals**: Use DaisyUI `modal` with custom backdrop ### 5.3 Icons Use FontAwesome 7 with consistent sizing: - Navigation: `text-sm` - Buttons: Default size - Headers: `text-lg` to `text-2xl` --- ## 6. Common Helpers ### 6.1 camel_to_snake_array (PHP Helper) Convert JavaScript camelCase keys to PHP snake_case: ```php function camel_to_snake_array(array $data): array { $result = []; foreach ($data as $key => $value) { $snakeKey = strtolower(preg_replace('/[A-Z]/', '_$0', lcfirst($key))); $result[$snakeKey] = is_array($value) ? camel_to_snake_array($value) : $value; } return $result; } ``` ### 6.2 base_url() Usage Always use `base_url()` for asset and API paths: ```php // In PHP // In JavaScript (via global variable) ``` --- ## 7. Quick Reference: Creating a New Module ### Step 1: Create Route ```php // app/Config/Routes.php $routes->get('/products', 'PagesController::products'); $routes->get('api/products', 'ProductsController::index'); $routes->get('api/products/(:num)', 'ProductsController::show/$1'); $routes->post('api/products', 'ProductsController::create'); $routes->patch('api/products/(:num)', 'ProductsController::update/$1'); ``` ### Step 2: Create Model ```php // app/Models/ProductsModel.php namespace App\Models; class ProductsModel extends BaseModel { protected $table = 'products'; protected $primaryKey = 'product_id'; protected $allowedFields = ['name', 'sku', 'price']; } ``` ### Step 3: Create Controller Copy pattern from `PatientsController.php` or `RequestsController.php`. ### Step 4: Create View Create `app/Views/products/products_index.php` following the page view pattern. ### Step 5: Create Dialog Create `app/Views/products/dialog_form.php` for add/edit modal. ### Step 6: Add to Navigation Update `app/Views/layout/main_layout.php` sidebar. --- ## 8. Things to Avoid 1. **Don't use jQuery** - Use Alpine.js or vanilla JS 2. **Don't over-engineer** - Keep it simple 3. **Don't skip soft deletes** - Always use `deleted_at` 4. **Don't hardcode URLs** - Use `base_url()` and `BASEURL` 5. **Don't mix concerns** - Controllers handle HTTP, Models handle data 6. **Don't create separate JS files for each page** - Keep JS inline in views for simplicity --- ## 9. Checklist Before Deploying - [ ] All routes added to `Routes.php` - [ ] API responses follow standard format - [ ] Form validation in both frontend and backend - [ ] Error handling with user-friendly messages - [ ] Loading states for async operations - [ ] Responsive design tested - [ ] README.md updated