- CodeIgniter 4 framework setup with SQL Server database config - Models: Control, Test, Dept, Result, Daily/ Monthly entry models - Controllers: Dashboard, Control, Test, Dept, Entry, Report, API endpoints - Views: CRUD pages with modal dialogs, dashboard, reports - Database: Migrations for control test and daily/monthly result tables - Legacy v1 PHP application preserved in /v1 directory - Documentation: AGENTS.md, VIEWS_RULES.md for development guidelines
9.7 KiB
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
# 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
<?phpopening 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) notelse 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:
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:
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:
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:
<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:
$builder = $this->db->table('results'); $builder->select('*'); $builder->where('control_ref_id', $controlId); $results = $builder->get()->getResultArray(); - For SQL Server, set
DBDriverto'SQLSRV'in config
Error Handling
- Use CodeIgniter's exception handling:
throw new \CodeIgniter\Exceptions\PageNotFoundException('Not found') - Return JSON responses for AJAX endpoints:
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:
<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
Appobject 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-datawith an object containing all properties and methods - Example:
<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_addor_editsuffix)
Controller Pattern:
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
$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):
<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
.envfiles with credentials
Common Operations
Add a new CRUD resource:
- Create model in
app/Models/ - Create controller in
app/Controllers/ - Add routes in
app/Config/Routes.php - 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_menuparameter in view() call