341 lines
9.7 KiB
Markdown
341 lines
9.7 KiB
Markdown
|
|
# AGENTS.md - QC Application Development Guide
|
||
|
|
|
||
|
|
This document provides guidelines for agentic coding agents working on this PHP QC (Quality Control) application built with CodeIgniter 4.
|
||
|
|
|
||
|
|
## Project Overview
|
||
|
|
|
||
|
|
This is a CodeIgniter 4 PHP application using SQL Server database for quality control data management. The app handles control tests, daily/monthly entries, and reporting. Uses Tailwind CSS and Alpine.js for UI.
|
||
|
|
|
||
|
|
## Build/Lint/Test Commands
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# PHP syntax check single file
|
||
|
|
php -l app/Controllers/Dashboard.php
|
||
|
|
|
||
|
|
# PHP syntax check all files recursively
|
||
|
|
find . -name "*.php" -exec php -l {} \; 2>&1 | grep -v "No syntax errors"
|
||
|
|
|
||
|
|
# Run all PHPUnit tests
|
||
|
|
./vendor/bin/phpunit
|
||
|
|
|
||
|
|
# Run single test class
|
||
|
|
./vendor/bin/phpunit tests/unit/HealthTest.php
|
||
|
|
|
||
|
|
# Run single test method
|
||
|
|
./vendor/bin/phpunit tests/unit/HealthTest.php --filter=testIsDefinedAppPath
|
||
|
|
|
||
|
|
# Run with coverage report
|
||
|
|
./vendor/bin/phpunit --coverage-html coverage/
|
||
|
|
|
||
|
|
# Start development server
|
||
|
|
php spark serve
|
||
|
|
```
|
||
|
|
|
||
|
|
## Code Style Guidelines
|
||
|
|
|
||
|
|
### General Principles
|
||
|
|
|
||
|
|
- Follow CodeIgniter 4 MVC patterns
|
||
|
|
- Maintain consistency with surrounding code
|
||
|
|
- Keep files focused (<200 lines preferred)
|
||
|
|
- Use clear, descriptive names
|
||
|
|
|
||
|
|
### PHP Style
|
||
|
|
|
||
|
|
- Use `<?php` opening tag (not `<?` short tags)
|
||
|
|
- Enable strict types: `declare(strict_types=1);` at top of PHP files
|
||
|
|
- Use strict comparison (`===`, `!==`) over loose comparison
|
||
|
|
- Use `elseif` (one word) not `else if`
|
||
|
|
- Use parentheses with control structures for single statements
|
||
|
|
- Use 4 spaces for indentation (not tabs)
|
||
|
|
- Return types and typed properties required for new code:
|
||
|
|
```php
|
||
|
|
public function index(): string
|
||
|
|
{
|
||
|
|
return view('layout', [...]);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Naming Conventions
|
||
|
|
|
||
|
|
- Classes: `PascalCase` (e.g., `Dashboard`, `DictTestModel`)
|
||
|
|
- Methods/functions: `$camelCase` (e.g., `getWithDept`, `saveResult`)
|
||
|
|
- Variables: `$camelCase` (e.g., `$dictTestModel`, `$resultData`)
|
||
|
|
- Constants: `UPPER_SNAKE_CASE`
|
||
|
|
- Views: `lowercase_with_underscores` (e.g., `dashboard.php`, `test_index.php`)
|
||
|
|
- Routes: kebab-case URLs (e.g., `/test/edit/(:num)` → `/test/edit/1`)
|
||
|
|
|
||
|
|
### CodeIgniter 4 Patterns
|
||
|
|
|
||
|
|
**Controllers** extend `App\Controllers\BaseController`:
|
||
|
|
```php
|
||
|
|
namespace App\Controllers;
|
||
|
|
|
||
|
|
use App\Models\DictTestModel;
|
||
|
|
|
||
|
|
class Test extends BaseController
|
||
|
|
{
|
||
|
|
protected $dictTestModel;
|
||
|
|
|
||
|
|
public function __construct()
|
||
|
|
{
|
||
|
|
$this->dictTestModel = new DictTestModel();
|
||
|
|
}
|
||
|
|
|
||
|
|
public function index(): string
|
||
|
|
{
|
||
|
|
$data = [
|
||
|
|
'title' => 'Test Dictionary',
|
||
|
|
'tests' => $this->dictTestModel->findAll(),
|
||
|
|
];
|
||
|
|
|
||
|
|
return view('layout', [
|
||
|
|
'content' => view('test/index', $data),
|
||
|
|
'page_title' => 'Test Dictionary',
|
||
|
|
'active_menu' => 'test'
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Models** extend `CodeIgniter\Model`:
|
||
|
|
```php
|
||
|
|
namespace App\Models;
|
||
|
|
|
||
|
|
use CodeIgniter\Model;
|
||
|
|
|
||
|
|
class TestModel extends Model
|
||
|
|
{
|
||
|
|
protected $table = 'dict_test';
|
||
|
|
protected $primaryKey = 'id';
|
||
|
|
protected $returnType = 'array';
|
||
|
|
protected $useSoftDeletes = false;
|
||
|
|
protected $allowedFields = ['deptid', 'name', 'unit', 'method'];
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Views** use PHP short tags `<?= ?>` for output:
|
||
|
|
```php
|
||
|
|
<div class="space-y-6">
|
||
|
|
<h1 class="text-2xl font-bold"><?= $title ?></h1>
|
||
|
|
<?php foreach ($tests as $test): ?>
|
||
|
|
<div><?= $test['name'] ?></div>
|
||
|
|
<?php endforeach; ?>
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
### Database Operations
|
||
|
|
|
||
|
|
- Configure connections in `app/Config/Database.php`
|
||
|
|
- Use CodeIgniter's Query Builder for queries
|
||
|
|
- Use parameterized queries to prevent SQL injection:
|
||
|
|
```php
|
||
|
|
$builder = $this->db->table('results');
|
||
|
|
$builder->select('*');
|
||
|
|
$builder->where('control_ref_id', $controlId);
|
||
|
|
$results = $builder->get()->getResultArray();
|
||
|
|
```
|
||
|
|
- For SQL Server, set `DBDriver` to `'SQLSRV'` in config
|
||
|
|
|
||
|
|
### Error Handling
|
||
|
|
|
||
|
|
- Use CodeIgniter's exception handling: `throw new \CodeIgniter\Exceptions\PageNotFoundException('Not found')`
|
||
|
|
- Return JSON responses for AJAX endpoints:
|
||
|
|
```php
|
||
|
|
return $this->response->setJSON(['success' => true, 'data' => $results]);
|
||
|
|
```
|
||
|
|
- Use `log_message('error', $message)` for logging
|
||
|
|
- Validate input with `$this->validate()` in controllers
|
||
|
|
|
||
|
|
### Frontend (Tailwind CSS + Alpine.js)
|
||
|
|
|
||
|
|
The layout already includes Tailwind via CDN and Alpine.js:
|
||
|
|
```html
|
||
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
||
|
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||
|
|
```
|
||
|
|
|
||
|
|
**Common JavaScript (`public/js/app.js`):**
|
||
|
|
- Contains global `App` object with shared utilities
|
||
|
|
- Handles sidebar toggle functionality
|
||
|
|
- Provides `App.showToast()`, `App.confirmSave()`, `App.closeAllModals()`
|
||
|
|
- Initialize with `App.init()` on DOMContentLoaded
|
||
|
|
|
||
|
|
**Page-Specific Alpine.js:**
|
||
|
|
- Complex Alpine.js components should be defined inline in the view PHP file
|
||
|
|
- Use `x-data` with an object containing all properties and methods
|
||
|
|
- Example:
|
||
|
|
```html
|
||
|
|
<div x-data="{
|
||
|
|
property1: '',
|
||
|
|
property2: [],
|
||
|
|
|
||
|
|
init() {
|
||
|
|
// Initialization code
|
||
|
|
},
|
||
|
|
|
||
|
|
async loadData() {
|
||
|
|
// Fetch and handle data
|
||
|
|
App.showToast('Loaded successfully');
|
||
|
|
}
|
||
|
|
}">
|
||
|
|
<!-- Component markup -->
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
### File Organization
|
||
|
|
|
||
|
|
- Controllers: `app/Controllers/`
|
||
|
|
- Models: `app/Models/`
|
||
|
|
- Views: `app/Views/` (subfolders for modules: `entry/`, `test/`, `control/`, `report/`, `dept/`)
|
||
|
|
- Config: `app/Config/`
|
||
|
|
- Routes: `app/Config/Routes.php`
|
||
|
|
|
||
|
|
### Modal-based CRUD Pattern
|
||
|
|
|
||
|
|
All CRUD operations use a single modal dialog file (`dialog.php`) that handles both add and edit modes.
|
||
|
|
|
||
|
|
**File Naming:**
|
||
|
|
- Single dialog: `dialog.php` (no `_add` or `_edit` suffix)
|
||
|
|
|
||
|
|
**Controller Pattern:**
|
||
|
|
```php
|
||
|
|
class Dept extends BaseController
|
||
|
|
{
|
||
|
|
protected $dictDeptModel;
|
||
|
|
|
||
|
|
public function __construct()
|
||
|
|
{
|
||
|
|
$this->dictDeptModel = new DictDeptModel();
|
||
|
|
}
|
||
|
|
|
||
|
|
public function index(): string
|
||
|
|
{
|
||
|
|
$data = [
|
||
|
|
'title' => 'Department Dictionary',
|
||
|
|
'depts' => $this->dictDeptModel->findAll(),
|
||
|
|
];
|
||
|
|
|
||
|
|
return view('layout', [
|
||
|
|
'content' => view('dept/index', $data),
|
||
|
|
'page_title' => 'Department Dictionary',
|
||
|
|
'active_menu' => 'dept'
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function save(): \CodeIgniter\HTTP\RedirectResponse
|
||
|
|
{
|
||
|
|
$data = ['name' => $this->request->getPost('name')];
|
||
|
|
$this->dictDeptModel->insert($data);
|
||
|
|
return redirect()->to('/dept');
|
||
|
|
}
|
||
|
|
|
||
|
|
public function update($id): \CodeIgniter\HTTP\RedirectResponse
|
||
|
|
{
|
||
|
|
$data = ['name' => $this->request->getPost('name')];
|
||
|
|
$this->dictDeptModel->update($id, $data);
|
||
|
|
return redirect()->to('/dept');
|
||
|
|
}
|
||
|
|
|
||
|
|
public function delete($id): \CodeIgniter\HTTP\RedirectResponse
|
||
|
|
{
|
||
|
|
$this->dictDeptModel->delete($id);
|
||
|
|
return redirect()->to('/dept');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Dialog View Pattern (`dialog.php`):**
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
$isEdit = isset($record);
|
||
|
|
$action = $isEdit ? '/controller/update/' . $record['id'] : '/controller/save';
|
||
|
|
$title = $isEdit ? 'Edit Title' : 'Add Title';
|
||
|
|
?>
|
||
|
|
|
||
|
|
<div x-data="{ open: false }">
|
||
|
|
<?php if (!$isEdit): ?>
|
||
|
|
<button @click="open = true" class="...">Add</button>
|
||
|
|
<?php endif; ?>
|
||
|
|
|
||
|
|
<div x-show="open" class="fixed inset-0 z-50 ..." style="display: none;">
|
||
|
|
<h2 class="text-xl font-bold mb-4"><?= $title ?></h2>
|
||
|
|
<form action="<?= $action ?>" method="post">
|
||
|
|
<!-- Form fields with values from $record if edit mode -->
|
||
|
|
<input type="text" name="name" value="<?= $record['name'] ?? '' ?>">
|
||
|
|
<button type="submit"><?= $isEdit ? 'Update' : 'Save' ?></button>
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
**Index View Pattern (`index.php`):**
|
||
|
|
```php
|
||
|
|
<div class="space-y-6">
|
||
|
|
<!-- Table with data attributes for edit/delete -->
|
||
|
|
<table>
|
||
|
|
<tbody>
|
||
|
|
<?php foreach ($records as $record): ?>
|
||
|
|
<tr>
|
||
|
|
<td><?= $record['name'] ?></td>
|
||
|
|
<td>
|
||
|
|
<button data-edit-id="<?= $record['id'] ?>"
|
||
|
|
data-name="<?= $record['name'] ?>"
|
||
|
|
class="...">Edit</button>
|
||
|
|
<button data-delete-id="<?= $record['id'] ?>"
|
||
|
|
class="...">Delete</button>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
<?php endforeach; ?>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Dialog for add (no data passed) -->
|
||
|
|
<?= view('module/dialog') ?>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
// All modal-related JS in index.php
|
||
|
|
document.querySelectorAll('[data-edit-id]').forEach(btn => {
|
||
|
|
btn.addEventListener('click', function() {
|
||
|
|
document.getElementById('editId').value = this.dataset.editId;
|
||
|
|
document.getElementById('editName').value = this.dataset.name;
|
||
|
|
document.getElementById('editModal').classList.remove('hidden');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
document.querySelectorAll('[data-delete-id]').forEach(btn => {
|
||
|
|
btn.addEventListener('click', function() {
|
||
|
|
if (confirm('Delete this record?')) {
|
||
|
|
window.location.href = '/controller/delete/' + this.dataset.deleteId;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
```
|
||
|
|
|
||
|
|
**Searchable Multi-Select:**
|
||
|
|
- Use Select2 for searchable dropdowns (already included in layout.php)
|
||
|
|
- Initialize with: `$('#selectId').select2({ placeholder: 'Search...', allowClear: true })`
|
||
|
|
|
||
|
|
### Security Considerations
|
||
|
|
|
||
|
|
- Use CodeIgniter's built-in CSRF protection (`$this->validate('csrf')`)
|
||
|
|
- Escape all output in views with `<?= ?>` (auto-escaped)
|
||
|
|
- Use `$this->request->getPost()` instead of `$_POST`
|
||
|
|
- Never commit `.env` files with credentials
|
||
|
|
|
||
|
|
## Common Operations
|
||
|
|
|
||
|
|
**Add a new CRUD resource:**
|
||
|
|
1. Create model in `app/Models/`
|
||
|
|
2. Create controller in `app/Controllers/`
|
||
|
|
3. Add routes in `app/Config/Routes.php`
|
||
|
|
4. Create views in `app/Views/[module]/`
|
||
|
|
|
||
|
|
**Add a menu item:**
|
||
|
|
- Add to sidebar in `app/Views/layout.php`
|
||
|
|
- Add route in `app/Config/Routes.php`
|
||
|
|
- Create controller method
|
||
|
|
- Set `active_menu` parameter in view() call
|