# AGENTS.md - AI Agent Guidelines for [PROJECT NAME]
## AI Agent Guidelines
1. **Readability**: Write code that is easy to read and understand.
2. **Maintainability**: Write code that is easy to maintain and update.
3. **Performance**: Write code that is fast and efficient.
4. **Security**: Write code that is secure and protected against attacks.
5. **Testing**: Write code that is tested and verified.
## Technology Stack
| Layer | Technology |
|-------|------------|
| Backend | CodeIgniter 4 (PHP 8.1+) |
| Frontend | Alpine.js + TailwindCSS |
| Database | MySQL/MariaDB |
## Key Files & Locations
### Backend
```
app/Controllers/ # API & page controllers
app/Models/ # Eloquent-style models
app/Database/Migrations/ # Schema definitions
app/Config/Routes.php # All routes defined here
app/Helpers/ # Helper functions
```
### Frontend
```
app/Views/ # PHP views with Alpine.js
app/Views/layout/ # Base templates
public/ # Static assets (css, js)
```
## Coding Conventions
### PHP / CodeIgniter 4
1. **Controllers** extend `BaseController` and use `ResponseTrait`
2. **Models** extend `App\Models\BaseModel` (custom base with auto camel/snake conversion)
3. **Soft deletes** are enabled on all tables (`deleted_at`)
4. **Timestamps** are automatic (`created_at`, `updated_at`)
5. **Validation** happens in controllers, not models
6. **JSON API responses** follow this structure:
```php
return $this->respond([
'status' => 'success',
'message' => 'fetch success',
'data' => $rows
], 200);
```
7. **Use camelCase for input/output**, snake_case for database:
- Controllers convert camel→snake before insert/update
- Models convert snake→camel after fetch
- Use helper functions: `camel_to_snake()`, `camel_to_snake_array()`, `snake_to_camel()`
### Database
1. **Primary keys**: `{table_singular}_id` (e.g., `item_id`, `pat_id`)
2. **Foreign keys**: Match the referenced primary key name
3. **Naming**: All lowercase, underscores
4. **Soft deletes**: All tables have `deleted_at` DATETIME column
5. **Master data tables**: Prefix with `master_` (e.g., `master_items`)
6. **Timestamps**: `created_at`, `updated_at` DATETIME columns
7. **Unique constraints**: Add on code fields (e.g., `item_code`)
### Frontend / Alpine.js
1. **x-data** on container elements
2. **Fetch API** for AJAX calls (no jQuery)
3. **DaisyUI components** for UI elements
4. **camelCase** for JavaScript, **snake_case** for PHP/DB
5. **Modals** with x-show and x-transition
6. **ES6 modules** importing Alpine from `app.js`
### File Naming
| Component | Pattern | Example |
|-----------|---------|---------|
| Controller | `PascalCase + Controller` | `ItemsController` |
| Model | `PascalCase + Model` | `ItemsModel` |
| Migration | `YYYY-MM-DD-XXXXXX_Description.php` | `2026-01-15-000001_Items.php` |
| View | `module/action.php` | `items/index.php` |
| Helper | `snake_case + _helper.php` | `stringcase_helper.php` |
| Filter | `PascalCase + Filter.php` | `JwtAuthFilter.php` |
### Directory Structure
```
app/
├── Config/
│ ├── Routes.php
│ └── Filters.php
├── Controllers/
│ ├── BaseController.php
│ ├── ItemsController.php
│ └── Master/
│ └── ItemsController.php
├── Database/
│ └── Migrations/
├── Filters/
│ └── JwtAuthFilter.php
├── Helpers/
│ ├── stringcase_helper.php
│ └── utc_helper.php
├── Models/
│ ├── BaseModel.php
│ ├── ItemsModel.php
│ └── Master/
│ └── ItemsModel.php
└── Views/
├── layout/
│ ├── main_layout.php
│ └── form_layout.php
├── items/
│ ├── items_index.php
│ └── dialog_items_form.php
└── login.php
```
## Common Tasks
### Adding a New Master Data Entity
1. Create migration in `app/Database/Migrations/`
2. Create model in `app/Models/[Module]/`
3. Create controller in `app/Controllers/[Module]/`
4. Add routes in `app/Config/Routes.php`
5. Create view in `app/Views/[module]/`
### Adding a New API Endpoint
```php
// In Routes.php
$routes->get('api/resource', 'ResourceController::index');
$routes->get('api/resource/(:num)', 'ResourceController::show/$1');
$routes->post('api/resource', 'ResourceController::create');
$routes->patch('api/resource/(:num)', 'ResourceController::update/$1');
```
### Controller Template
```php
model = new ItemsModel();
$this->rules = [
'itemCode' => 'required|min_length[1]',
'itemName' => 'required',
];
}
public function index() {
$keyword = $this->request->getGet('keyword');
try {
$rows = $this->model->getItems($keyword);
return $this->respond([
'status' => 'success',
'message' => 'fetch success',
'data' => $rows
], 200);
} catch (\Exception $e) {
return $this->failServerError('Exception: ' . $e->getMessage());
}
}
public function show($id = null) {
try {
$rows = $this->model->where('item_id', $id)->findAll();
if (empty($rows)) {
return $this->respond([
'status' => 'success',
'message' => 'data not found.'
], 200);
}
return $this->respond([
'status' => 'success',
'message' => 'fetch success',
'data' => $rows
], 200);
} catch (\Exception $e) {
return $this->failServerError('Something went wrong: ' . $e->getMessage());
}
}
public function create() {
$input = $this->request->getJSON(true);
$input = camel_to_snake_array($input);
if (!$this->validate($this->rules)) {
return $this->failValidationErrors($this->validator->getErrors());
}
try {
$id = $this->model->insert($input, true);
return $this->respondCreated([
'status' => 'success',
'message' => $id
]);
} catch (\Exception $e) {
return $this->failServerError('Something went wrong: ' . $e->getMessage());
}
}
public function update($id = null) {
$input = $this->request->getJSON(true);
$input = camel_to_snake_array($input);
if (!$this->validate($this->rules)) {
return $this->failValidationErrors($this->validator->getErrors());
}
try {
$this->model->update($id, $input);
return $this->respondCreated([
'status' => 'success',
'message' => 'update success',
'data' => $id
]);
} catch (\Exception $e) {
return $this->failServerError('Something went wrong: ' . $e->getMessage());
}
}
}
```
### Model Template
```php
groupStart()
->like('item_code', $keyword)
->orLike('item_name', $keyword)
->groupEnd()
->findAll();
}
return $this->findAll();
}
}
```
### Migration Template
```php
forge->addField([
'item_id' => [
'type' => 'int',
'unsigned' => true,
'auto_increment' => true
],
'item_code' => [
'type' => 'VARCHAR',
'constraint' => 50
],
'item_name' => [
'type' => 'VARCHAR',
'constraint' => 150
],
'created_at' => [
'type' => 'DATETIME',
'null' => true
],
'updated_at' => [
'type' => 'DATETIME',
'null' => true
],
'deleted_at' => [
'type' => 'DATETIME',
'null' => true
]
]);
$this->forge->addKey('item_id', true);
$this->forge->addUniqueKey('item_code');
$this->forge->createTable('module_items');
}
public function down() {
$this->forge->dropTable('module_items', true);
}
}
```
### Routes Template
```php
post('/login', 'AuthController::login');
$routes->get('/logout', 'AuthController::logout');
$routes->group('', ['filter' => 'jwt-auth'], function ($routes) {
$routes->get('/', 'PagesController::dashboard');
$routes->get('/module', 'PagesController::module');
$routes->get('/master/items', 'PagesController::masterItems');
});
$routes->group('api', function ($routes) {
$routes->get('module/items', 'Module\ItemsController::index');
$routes->get('module/items/(:num)', 'Module\ItemsController::show/$1');
$routes->post('module/items', 'Module\ItemsController::create');
$routes->patch('module/items/(:num)', 'Module\ItemsController::update/$1');
});
```
### View Template (Index with Alpine.js)
```php
= $this->extend("layout/main_layout"); ?>
= $this->section("content"); ?>
= $this->include('module/items/dialog_items_form'); ?>
= $this->endSection(); ?>
= $this->section("script"); ?>
= $this->endSection(); ?>
```
### Running Migrations
```bash
php spark migrate # Run all pending
php spark migrate:rollback # Rollback last batch
php spark migrate:refresh # Rollback all + re-run
```
## Testing
```bash
# Run all tests
./vendor/bin/phpunit
# Run specific test file
./vendor/bin/phpunit tests/unit/SomeTest.php
```
## Things to Avoid
1. **Don't use jQuery** - Use Alpine.js or vanilla JS
2. **Don't over-engineer** - This is a "no-nonsense" project
3. **Don't skip soft deletes** - Always use `deleted_at`
4. **Don't hardcode** - Use `.env` for configuration
5. **Don't mix concerns** - Controllers handle HTTP, Models handle data
6. **Don't forget camel/snake conversion** - Frontend uses camelCase, DB uses snake_case
## Questions to Ask Before Making Changes
1. Does this follow the existing patterns?
2. Is there a simpler way to do this?
3. Did I add the route?
4. Did I handle errors gracefully?
5. Does the API response match the standard format?
6. Did I convert camelCase to snake_case before DB operations?
7. Did I convert snake_case to camelCase after fetching?
## Post-Change Requirements
**After every significant code change, update `README.md`:**
| Change Type | README Section to Update |
|-------------|--------------------------|
| New Controller | Project Structure, API Endpoints |
| New Model | Project Structure |
| New Migration/Table | Database Schema |
| New View | Project Structure |
| New Route | API Endpoints |
| New Command | Development Commands |
| Config Change | Setup |
Keep the README accurate and up-to-date with the actual codebase.