CodeIgniter
CodeIgniter 4.x rules and best practices for Cursor
cursor.directory·↓ 15
规则
codeigniter
CodeIgniter 4.x rules and best practices for Cursor
# CodeIgniter 4 — Clean Code Skill
> Expert guidance for writing readable, maintainable, and testable CodeIgniter 4 / PHP 8.1+ code.
---
## Core Philosophy
- **Readable first**: Code is read far more than it is written. Optimize for the next developer.
- **Small, focused units**: Every class, method, and function does one thing only (SRP).
- **Explicit over implicit**: Avoid magic. Make intent visible through naming and structure.
- **No premature optimization**: Write clear code first; optimize only when profiling justifies it.
- **Fail loudly**: Prefer exceptions over silent failures. Never swallow errors.
---
## PHP Standards
- Always declare `declare(strict_types=1);` at the top of every file.
- Use PHP 8.1+ features: `readonly` properties, enums, named arguments, fibers, intersection types.
- Follow PSR-12 for code style.
- Use typed properties, typed parameters, and explicit return types on **every** method — including `void`.
- Never use `mixed` unless absolutely unavoidable; document why when you do.
- Prefer `readonly` classes and properties for value objects and DTOs.
- Use named arguments when calling functions with multiple parameters to improve clarity.
```php
// Bad
$user = new User('John', 28, true);
// Good
$user = new User(name: 'John', age: 28, isActive: true);
```
---
## Naming Rules
> The most important clean code rule.
- **Classes**: PascalCase, noun or noun phrase — `UserRepository`, `CreateOrderService`, `PaymentGatewayInterface`.
- **Methods**: camelCase, verb or verb phrase — `findById()`, `createOrder()`, `isEmailTaken()`.
- **Variables**: camelCase, meaningful nouns — `$activeUsers`, `$orderTotal`. Never `$data`, `$arr`, `$temp`, `$x`.
- **Booleans**: prefix with `is`, `has`, `can`, `should` — `$isActive`, `$hasPermission`, `$canRefund`.
- **Constants / Enums**: UPPER_SNAKE for constants, PascalCase for enum cases.
- **No abbreviations**: write `$userIdentifier`, not `$uid`. Write `getUserByEmail()`, not `getUsrByEml()`.
- **Length rule**: if a name needs a comment to explain it, rename it instead.
```php
// Bad
$d = $u->getData(); // get user data
// Good
$userProfile = $user->getProfile();
```
---
## Clean Code Rules
### Functions and Methods
- **Maximum 20 lines** per method. If it's longer, extract.
- **Maximum 3 parameters**. If you need more, introduce a DTO or value object.
- **One level of abstraction per method**: don't mix high-level logic with low-level implementation.
- **No boolean flag parameters** — they indicate a method does two things.
- **No output arguments** — functions should return values, not modify passed references.
- **Command Query Separation (CQS)**: a method either changes state (command) or returns data (query), never both.
```php
// Bad — boolean flag, does two things
public function getUsers(bool $includeInactive): array { ... }
// Good — two explicit, clear methods
public function getActiveUsers(): array { ... }
public function getAllUsers(): array { ... }
```
### Classes
- **Single Responsibility**: one reason to change per class.
- Mark all classes `final` unless designed for extension.
- Use `readonly` for value objects and DTOs.
- Keep classes under **200 lines**. Longer classes are doing too much.
- Declare properties explicitly with types; never rely on dynamic property assignment.
- No public mutable properties — expose state through methods.
### Comments
- **Never comment what the code does** — write code that explains itself.
- Use comments only to explain **why**, not **what**.
- Prefer descriptive method extraction over inline comments.
- PHPDoc blocks for public API methods; skip them for obvious private methods.
```php
// Bad
// Loop through users and send email
foreach ($users as $user) { ... }
// Good — the method name IS the comment
$this->notifyUsersOfUpcomingExpiry($users);
```
---
## Architecture
### Directory Structure
```
app/
├── Controllers/
│ └── Api/V1/
├── DTO/ ← Input/output data shapes
├── Enums/ ← PHP 8.1 enums
├── Exceptions/ ← Domain-specific exceptions
├── Filters/ ← Middleware equivalents
├── Interfaces/ ← Contracts for services and repos
├── Models/ ← CI4 Models (DB layer only)
├── Repositories/ ← Data access abstraction
├── Services/ ← Business logic
└── ValueObjects/ ← Immutable domain primitives
```
### Layers and Responsibilities
#### Controllers — HTTP only. No business logic.
- Validate the HTTP request.
- Call one service method.
- Return the HTTP response.
- Must be `final`.
```php
final class UsersController extends ResourceController
{
public function __construct(
private readonly UserService $userService
) {}
public function show(int $id): ResponseInterface
{
$user = $this->userService->findById($id);
return $this->respond($user->toArray());
}
}
```
#### Services — Business logic only. No HTTP, no SQL.
- Orchestrate domain operations.
- Call repositories, not models directly.
- Must be `final` and `readonly`.
- Throw domain exceptions on failure.
```php
final class UserService
{
public function __construct(
private readonly UserRepositoryInterface $userRepository,
private readonly MailerInterface $mailer
) {}
public function register(RegisterUserDTO $dto): User
{
if ($this->userRepository->existsByEmail($dto->email)) {
throw new EmailAlreadyTakenException($dto->email);
}
$user = $this->userRepository->save(User::fromDTO($dto));
$this->mailer->sendWelcome($user);
return $user;
}
}
```
#### Repositories — Data access only. No business logic.
- Always program to an interface.
- Return domain objects or collections, never raw arrays from DB.
- Encapsulate all query logic; no Query Builder outside the repository.
```php
interface UserRepositoryInterface
{
public function findById(int $id): User;
public function existsByEmail(string $email): bool;
public function save(User $user): User;
}
```
#### DTOs — Data transfer only. Immutable.
```php
readonly class RegisterUserDTO
{
public function __construct(
public string $name,
public string $email,
public string $password,
) {}
public static function fromRequest(IncomingRequest $request): self
{
return new self(
name: $request->getVar('name'),
email: $request->getVar('email'),
password: $request->getVar('password'),
);
}
}
```
#### Value Objects — Immutable domain primitives with self-validation.
```php
readonly class Email
{
public function __construct(public readonly string $value)
{
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidEmailException($value);
}
}
public function equals(Email $other): bool
{
return $this->value === $other->value;
}
}
```
#### Enums — Replace magic strings and integers.
```php
enum OrderStatus: string
{
case Pending = 'pending';
case Confirmed = 'confirmed';
case Shipped = 'shipped';
case Cancelled = 'cancelled';
public function canTransitionTo(self $next): bool
{
return match($this) {
self::Pending => $next === self::Confirmed || $next === self::Cancelled,
self::Confirmed => $next === self::Shipped || $next === self::Cancelled,
default => false,
};
}
}
```
---
## Error Handling
- **Never return null to signal failure** — throw a named exception.
- Create domain-specific exceptions under `app/Exceptions/`.
- Name exceptions after what happened: `UserNotFoundException`, `InsufficientStockException`.
- Never catch `\Exception` generically unless at the top-level handler.
- Log at the boundary (controller / exception handler), not deep in services.
```php
// Bad
public function findById(int $id): ?User
{
return $this->db->find($id); // null means "not found" — ambiguous
}
// Good
public function findById(int $id): User
{
$record = $this->db->find($id);
if ($record === null) {
throw new UserNotFoundException($id);
}
return User::fromRecord($record);
}
```
---
## Validation
- Always validate at the HTTP boundary — never deep inside services.
- Use CodeIgniter's Validation service or a dedicated `FormRequest`-style class.
- Keep validation rules on the DTO or a dedicated `*Rules` class, not inline in the controller.
```php
final class RegisterUserRules
{
public static function rules(): array
{
return [
'name' => 'required|min_length[2]|max_length[100]',
'email' => 'required|valid_email|is_unique[users.email]',
'password' => 'required|min_length[8]',
];
}
public static function messages(): array
{
return [
'email' => ['is_unique' => 'This email is already registered.'],
];
}
}
```
---
## CodeIgniter 4 Specifics
- Register services in `app/Config/Services.php` with explicit type hints.
- Use Filters (`app/Filters/`) for auth, rate limiting, CORS — never put this logic in controllers.
- Use `$db->transStart()` / `$db->transComplete()` wrapped in try-catch for all multi-step writes.
- Use CodeIgniter Shield for authentication; never hand-roll auth.
- Use `cache()` helper with explicit TTL and tagged cache keys for invalidation.
- Prefix all CLI commands with `spark`; keep them in `app/Commands/`.
- Never call `model()` global helper inside services — inject the repository.
---
## Testing Rules
- Every service class must have a unit test.
- Every controller endpoint must have a feature test.
- Test method names: `test_it_throws_when_email_is_already_taken()`.
- Prefer fakes and stubs over mocks when possible.
- No test should touch the real database — use transactions or an in-memory SQLite DB.
- Keep tests under 30 lines; extract helpers to `setUp()` or factory methods.
---
## What to Always Avoid
| Avoid | Use instead |
|---|---|
| `$data`, `$result`, `$arr` | Descriptive names |
| `mixed` return types | Explicit types or generics |
| Raw SQL in controllers | Repository methods |
| `null` as error signal | Named exceptions |
| God classes (500+ lines) | Split by responsibility |
| Boolean flag parameters | Two separate methods |
| Comments explaining *what* | Self-documenting method names |
| Magic numbers/strings | Enums or named constants |
| `model()` global in services | Constructor injection |
| Silent `catch` blocks | Log and rethrow or handle explicitly |
---
## Dependencies
- PHP 8.1+
- CodeIgniter 4.x
- Composer for dependency management
- `codeigniter4/shield` for authentication
- PHPUnit for testing